Browse Source

added IHasImages and IHasUserData

Luke Pulverenti 11 years ago
parent
commit
cd859ac2e6
59 changed files with 1290 additions and 650 deletions
  1. 10 178
      MediaBrowser.Api/Images/ImageService.cs
  2. 2 3
      MediaBrowser.Api/Images/ImageWriter.cs
  3. 1 1
      MediaBrowser.Api/ItemUpdateService.cs
  4. 184 0
      MediaBrowser.Api/LiveTv/LiveTvImageService.cs
  5. 1 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  6. 56 68
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  7. 4 5
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  8. 1 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  9. 10 64
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  10. 3 6
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  11. 16 7
      MediaBrowser.Api/Playback/StreamState.cs
  12. 1 1
      MediaBrowser.Api/SearchService.cs
  13. 42 0
      MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs
  14. 7 0
      MediaBrowser.Common/IO/IFileSystem.cs
  15. 4 4
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  16. 1 1
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  17. 30 39
      MediaBrowser.Controller/Entities/BaseItem.cs
  18. 97 0
      MediaBrowser.Controller/Entities/IHasImages.cs
  19. 15 0
      MediaBrowser.Controller/Entities/IHasUserData.cs
  20. 3 1
      MediaBrowser.Controller/Entities/TV/Series.cs
  21. 1 1
      MediaBrowser.Controller/Library/IUserDataManager.cs
  22. 1 1
      MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
  23. 0 75
      MediaBrowser.Controller/LiveTv/Channel.cs
  24. 9 3
      MediaBrowser.Controller/LiveTv/ChannelInfo.cs
  25. 9 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  26. 3 3
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  27. 57 0
      MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
  28. 33 0
      MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
  29. 43 0
      MediaBrowser.Controller/LiveTv/LiveTvRecording.cs
  30. 16 4
      MediaBrowser.Controller/LiveTv/ProgramInfo.cs
  31. 9 3
      MediaBrowser.Controller/LiveTv/RecordingInfo.cs
  32. 5 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  33. 8 18
      MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
  34. 29 11
      MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs
  35. 4 4
      MediaBrowser.Controller/Providers/IImageEnhancer.cs
  36. 2 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  37. 14 0
      MediaBrowser.Model/LiveTv/ProgramInfoDto.cs
  38. 14 0
      MediaBrowser.Model/LiveTv/RecordingInfoDto.cs
  39. 6 0
      MediaBrowser.Model/LiveTv/RecordingQuery.cs
  40. 9 9
      MediaBrowser.Providers/ImageFromMediaLocationProvider.cs
  41. 2 2
      MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs
  42. 1 1
      MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs
  43. 1 1
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  44. 1 1
      MediaBrowser.Providers/Savers/ChannelXmlSaver.cs
  45. 7 7
      MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs
  46. 4 4
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  47. 1 1
      MediaBrowser.Server.Implementations/Library/UserDataManager.cs
  48. 55 22
      MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
  49. 81 33
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  50. 85 29
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  51. 136 0
      MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
  52. 136 0
      MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
  53. 2 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  54. 8 24
      MediaBrowser.Server.Implementations/Providers/ImageSaver.cs
  55. 1 1
      MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs
  56. 4 4
      MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs
  57. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  58. 1 1
      Nuget/MediaBrowser.Common.nuspec
  59. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 10 - 178
MediaBrowser.Api/Images/ImageService.cs

@@ -1,17 +1,17 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using ServiceStack;
 using ServiceStack;
+using ServiceStack.Text.Controller;
+using ServiceStack.Web;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Drawing;
 using System.Drawing;
@@ -19,8 +19,6 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using ServiceStack.Text.Controller;
-using ServiceStack.Web;
 
 
 namespace MediaBrowser.Api.Images
 namespace MediaBrowser.Api.Images
 {
 {
@@ -39,18 +37,6 @@ namespace MediaBrowser.Api.Images
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
-    [Route("/LiveTv/Channels/{Id}/Images", "GET")]
-    [Api(Description = "Gets information about an item's images")]
-    public class GetChannelImageInfos : 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("/Artists/{Name}/Images", "GET")]
     [Route("/Artists/{Name}/Images", "GET")]
     [Route("/Genres/{Name}/Images", "GET")]
     [Route("/Genres/{Name}/Images", "GET")]
     [Route("/GameGenres/{Name}/Images", "GET")]
     [Route("/GameGenres/{Name}/Images", "GET")]
@@ -80,20 +66,7 @@ namespace MediaBrowser.Api.Images
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
-
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "GET")]
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "GET")]
-    [Api(Description = "Gets an item image")]
-    public class GetChannelImage : ImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
+    
     /// <summary>
     /// <summary>
     /// Class UpdateItemImageIndex
     /// Class UpdateItemImageIndex
     /// </summary>
     /// </summary>
@@ -270,19 +243,6 @@ namespace MediaBrowser.Api.Images
         public Guid Id { get; set; }
         public Guid Id { get; set; }
     }
     }
 
 
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "DELETE")]
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "DELETE")]
-    [Api(Description = "Deletes an item image")]
-    public class DeleteChannelImage : DeleteImageRequest, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
     /// <summary>
     /// <summary>
     /// Class PostUserImage
     /// Class PostUserImage
     /// </summary>
     /// </summary>
@@ -358,38 +318,13 @@ namespace MediaBrowser.Api.Images
         public Stream RequestStream { get; set; }
         public Stream RequestStream { get; set; }
     }
     }
 
 
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "POST")]
-    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "POST")]
-    [Api(Description = "Posts an item image")]
-    public class PostChannelImage : DeleteImageRequest, IRequiresRequestStream, 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 = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The raw Http Request Input Stream
-        /// </summary>
-        /// <value>The request stream.</value>
-        public Stream RequestStream { get; set; }
-    }
-
     /// <summary>
     /// <summary>
     /// Class ImageService
     /// Class ImageService
     /// </summary>
     /// </summary>
     public class ImageService : BaseApiService
     public class ImageService : BaseApiService
     {
     {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
 
 
-        /// <summary>
-        /// The _library manager
-        /// </summary>
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
 
 
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
@@ -400,12 +335,11 @@ namespace MediaBrowser.Api.Images
         private readonly IDtoService _dtoService;
         private readonly IDtoService _dtoService;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
 
 
-        private readonly ILiveTvManager _liveTv;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageService" /> class.
         /// Initializes a new instance of the <see cref="ImageService" /> class.
         /// </summary>
         /// </summary>
-        public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor, ILiveTvManager liveTv)
+        public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
@@ -414,7 +348,6 @@ namespace MediaBrowser.Api.Images
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _dtoService = dtoService;
             _dtoService = dtoService;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
-            _liveTv = liveTv;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -431,15 +364,6 @@ namespace MediaBrowser.Api.Images
             return ToOptimizedResult(result);
             return ToOptimizedResult(result);
         }
         }
 
 
-        public object Get(GetChannelImageInfos request)
-        {
-            var item = _liveTv.GetChannel(request.Id);
-
-            var result = GetItemImageInfos(item);
-
-            return ToOptimizedResult(result);
-        }
-
         public object Get(GetItemByNameImageInfos request)
         public object Get(GetItemByNameImageInfos request)
         {
         {
             var result = GetItemByNameImageInfos(request);
             var result = GetItemByNameImageInfos(request);
@@ -540,7 +464,7 @@ namespace MediaBrowser.Api.Images
             return list;
             return list;
         }
         }
 
 
-        private ImageInfo GetImageInfo(string path, BaseItem item, int? imageIndex, ImageType type)
+        private ImageInfo GetImageInfo(string path, IHasImages item, int? imageIndex, ImageType type)
         {
         {
             try
             try
             {
             {
@@ -567,13 +491,6 @@ namespace MediaBrowser.Api.Images
             }
             }
         }
         }
 
 
-        public object Get(GetChannelImage request)
-        {
-            var item = _liveTv.GetChannel(request.Id);
-
-            return GetImage(request, item);
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>
@@ -659,20 +576,6 @@ namespace MediaBrowser.Api.Images
             Task.WaitAll(task);
             Task.WaitAll(task);
         }
         }
 
 
-        public void Post(PostChannelImage request)
-        {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = pathInfo.GetArgumentValue<string>(2);
-
-            request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(4), true);
-
-            var item = _liveTv.GetChannel(id);
-
-            var task = PostImage(item, request.RequestStream, request.Type, Request.ContentType);
-
-            Task.WaitAll(task);
-        }
-
         /// <summary>
         /// <summary>
         /// Deletes the specified request.
         /// Deletes the specified request.
         /// </summary>
         /// </summary>
@@ -699,15 +602,6 @@ namespace MediaBrowser.Api.Images
             Task.WaitAll(task);
             Task.WaitAll(task);
         }
         }
 
 
-        public void Delete(DeleteChannelImage request)
-        {
-            var item = _liveTv.GetChannel(request.Id);
-
-            var task = item.DeleteImage(request.Type, request.Index);
-
-            Task.WaitAll(task);
-        }
-
         /// <summary>
         /// <summary>
         /// Deletes the specified request.
         /// Deletes the specified request.
         /// </summary>
         /// </summary>
@@ -762,71 +656,9 @@ namespace MediaBrowser.Api.Images
         /// <param name="newIndex">The new index.</param>
         /// <param name="newIndex">The new index.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception>
         /// <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)
+        private Task UpdateItemIndex(IHasImages item, ImageType type, int currentIndex, int newIndex)
         {
         {
-            string file1;
-            string file2;
-
-            if (type == ImageType.Screenshot)
-            {
-                var hasScreenshots = (IHasScreenshots)item;
-                file1 = hasScreenshots.ScreenshotImagePaths[currentIndex];
-                file2 = hasScreenshots.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)
-        {
-            Directory.CreateDirectory(_appPaths.TempDirectory);
-
-            var temp1 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
-            var temp2 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
-
-            // Copying over will fail against hidden files
-            RemoveHiddenAttribute(file1);
-            RemoveHiddenAttribute(file2);
-
-            File.Copy(file1, temp1);
-            File.Copy(file2, temp2);
-
-            File.Copy(temp1, file2, true);
-            File.Copy(temp2, file1, true);
-
-            File.Delete(temp1);
-            File.Delete(temp2);
-        }
-
-        private void RemoveHiddenAttribute(string path)
-        {
-            var currentFile = new FileInfo(path);
-
-            // This will fail if the file is hidden
-            if (currentFile.Exists)
-            {
-                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
-                {
-                    currentFile.Attributes &= ~FileAttributes.Hidden;
-                }
-            }
+            return item.SwapImages(type, currentIndex, newIndex);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -837,7 +669,7 @@ namespace MediaBrowser.Api.Images
         /// <returns>System.Object.</returns>
         /// <returns>System.Object.</returns>
         /// <exception cref="ResourceNotFoundException">
         /// <exception cref="ResourceNotFoundException">
         /// </exception>
         /// </exception>
-        private object GetImage(ImageRequest request, BaseItem item)
+        public object GetImage(ImageRequest request, IHasImages item)
         {
         {
             var imagePath = GetImagePath(request, item);
             var imagePath = GetImagePath(request, item);
 
 
@@ -926,7 +758,7 @@ namespace MediaBrowser.Api.Images
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        private string GetImagePath(ImageRequest request, BaseItem item)
+        private string GetImagePath(ImageRequest request, IHasImages item)
         {
         {
             var index = request.Index ?? 0;
             var index = request.Index ?? 0;
 
 
@@ -941,7 +773,7 @@ namespace MediaBrowser.Api.Images
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="mimeType">Type of the MIME.</param>
         /// <param name="mimeType">Type of the MIME.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        private async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
+        public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
         {
         {
             using (var reader = new StreamReader(inputStream))
             using (var reader = new StreamReader(inputStream))
             {
             {

+ 2 - 3
MediaBrowser.Api/Images/ImageWriter.cs

@@ -2,12 +2,11 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using ServiceStack;
+using ServiceStack.Web;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using ServiceStack.Web;
 
 
 namespace MediaBrowser.Api.Images
 namespace MediaBrowser.Api.Images
 {
 {
@@ -27,7 +26,7 @@ namespace MediaBrowser.Api.Images
         /// Gets or sets the item.
         /// Gets or sets the item.
         /// </summary>
         /// </summary>
         /// <value>The item.</value>
         /// <value>The item.</value>
-        public BaseItem Item { get; set; }
+        public IHasImages Item { get; set; }
         /// <summary>
         /// <summary>
         /// The original image date modified
         /// The original image date modified
         /// </summary>
         /// </summary>

+ 1 - 1
MediaBrowser.Api/ItemUpdateService.cs

@@ -146,7 +146,7 @@ namespace MediaBrowser.Api
 
 
         private async Task UpdateItem(UpdateChannel request)
         private async Task UpdateItem(UpdateChannel request)
         {
         {
-            var item = _liveTv.GetChannel(request.Id);
+            var item = _liveTv.GetInternalChannel(request.Id);
 
 
             UpdateItem(request, item);
             UpdateItem(request, item);
 
 

+ 184 - 0
MediaBrowser.Api/LiveTv/LiveTvImageService.cs

@@ -0,0 +1,184 @@
+using MediaBrowser.Api.Images;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using ServiceStack;
+using ServiceStack.Text.Controller;
+using ServiceStack.Web;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.LiveTv
+{
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "POST")]
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "POST")]
+    [Api(Description = "Posts an item image")]
+    public class PostChannelImage : DeleteImageRequest, IRequiresRequestStream, 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 = "POST")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+    }
+
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "DELETE")]
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "DELETE")]
+    [Api(Description = "Deletes an item image")]
+    public class DeleteChannelImage : DeleteImageRequest, IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+    }
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}", "GET")]
+    [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "GET")]
+    [Api(Description = "Gets an item image")]
+    public class GetChannelImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Recordings/{Id}/Images/{Type}", "GET")]
+    [Route("/LiveTv/Recordings/{Id}/Images/{Type}/{Index}", "GET")]
+    [Api(Description = "Gets an item image")]
+    public class GetRecordingImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Programs/{Id}/Images/{Type}", "GET")]
+    [Route("/LiveTv/Programs/{Id}/Images/{Type}/{Index}", "GET")]
+    [Api(Description = "Gets an item image")]
+    public class GetProgramImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Channels/{Id}/Images", "GET")]
+    [Api(Description = "Gets information about an item's images")]
+    public class GetChannelImageInfos : 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; }
+    }
+    
+    public class LiveTvImageService : BaseApiService
+    {
+        private readonly ILiveTvManager _liveTv;
+
+        private readonly IUserManager _userManager;
+
+        private readonly ILibraryManager _libraryManager;
+
+        private readonly IApplicationPaths _appPaths;
+
+        private readonly IProviderManager _providerManager;
+
+        private readonly IItemRepository _itemRepo;
+        private readonly IDtoService _dtoService;
+        private readonly IImageProcessor _imageProcessor;
+        
+        public LiveTvImageService(ILiveTvManager liveTv, IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor)
+        {
+            _liveTv = liveTv;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _appPaths = appPaths;
+            _providerManager = providerManager;
+            _itemRepo = itemRepo;
+            _dtoService = dtoService;
+            _imageProcessor = imageProcessor;
+        }
+
+        public object Get(GetChannelImageInfos request)
+        {
+            var item = _liveTv.GetInternalChannel(request.Id);
+
+            var result = GetImageService().GetItemImageInfos(item);
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetChannelImage request)
+        {
+            var item = _liveTv.GetInternalChannel(request.Id);
+
+            return GetImageService().GetImage(request, item);
+        }
+
+        public object Get(GetRecordingImage request)
+        {
+            var item = _liveTv.GetInternalRecording(request.Id, CancellationToken.None).Result;
+
+            return GetImageService().GetImage(request, item);
+        }
+
+        public void Post(PostChannelImage request)
+        {
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var id = pathInfo.GetArgumentValue<string>(2);
+
+            request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(4), true);
+
+            var item = _liveTv.GetInternalChannel(id);
+
+            var task = GetImageService().PostImage(item, request.RequestStream, request.Type, Request.ContentType);
+
+            Task.WaitAll(task);
+        }
+
+        public void Delete(DeleteChannelImage request)
+        {
+            var item = _liveTv.GetInternalChannel(request.Id);
+
+            var task = item.DeleteImage(request.Type, request.Index);
+
+            Task.WaitAll(task);
+        }
+
+        private ImageService GetImageService()
+        {
+            return new ImageService(_userManager, _libraryManager, _appPaths, _providerManager, _itemRepo, _dtoService,
+                _imageProcessor);
+        }
+    }
+}

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

@@ -90,6 +90,7 @@
     <Compile Include="Library\LibraryHelpers.cs" />
     <Compile Include="Library\LibraryHelpers.cs" />
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
+    <Compile Include="LiveTv\LiveTvImageService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LocalizationService.cs" />
     <Compile Include="LocalizationService.cs" />
     <Compile Include="MoviesService.cs" />
     <Compile Include="MoviesService.cs" />

+ 56 - 68
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,11 +1,9 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.MediaInfo;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
@@ -110,7 +108,7 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         protected virtual string GetOutputFileExtension(StreamState state)
         protected virtual string GetOutputFileExtension(StreamState state)
         {
         {
-            return Path.GetExtension(state.Url);
+            return Path.GetExtension(state.RequestedUrl);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -187,7 +185,7 @@ namespace MediaBrowser.Api.Playback
         {
         {
             var args = string.Empty;
             var args = string.Empty;
 
 
-            if (state.Item.LocationType == LocationType.Remote)
+            if (state.IsRemote)
             {
             {
                 return string.Empty;
                 return string.Empty;
             }
             }
@@ -308,7 +306,7 @@ namespace MediaBrowser.Api.Playback
 
 
             return args.Trim();
             return args.Trim();
         }
         }
-        
+
         /// <summary>
         /// <summary>
         /// If we're going to put a fixed size on the command line, this will calculate it
         /// If we're going to put a fixed size on the command line, this will calculate it
         /// </summary>
         /// </summary>
@@ -331,7 +329,7 @@ namespace MediaBrowser.Api.Playback
                     string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase))
                     string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks, performTextSubtitleConversion);
+                    assSubtitleParam = GetTextSubtitleParam(state, request.StartTimeTicks, performTextSubtitleConversion);
                 }
                 }
             }
             }
 
 
@@ -402,14 +400,14 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the text subtitle param.
         /// Gets the text subtitle param.
         /// </summary>
         /// </summary>
-        /// <param name="video">The video.</param>
-        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <param name="state">The state.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion)
+        protected string GetTextSubtitleParam(StreamState state, long? startTimeTicks, bool performConversion)
         {
         {
-            var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream, startTimeTicks, performConversion) : GetExtractedAssPath(video, subtitleStream, startTimeTicks, performConversion);
+            var path = state.SubtitleStream.IsExternal ? GetConvertedAssPath(state.MediaPath, state.SubtitleStream, startTimeTicks, performConversion) :
+                GetExtractedAssPath(state, startTimeTicks, performConversion);
 
 
             if (string.IsNullOrEmpty(path))
             if (string.IsNullOrEmpty(path))
             {
             {
@@ -422,22 +420,21 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the extracted ass path.
         /// Gets the extracted ass path.
         /// </summary>
         /// </summary>
-        /// <param name="video">The video.</param>
-        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <param name="state">The state.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        private string GetExtractedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion)
+        private string GetExtractedAssPath(StreamState state, long? startTimeTicks, bool performConversion)
         {
         {
             var offset = TimeSpan.FromTicks(startTimeTicks ?? 0);
             var offset = TimeSpan.FromTicks(startTimeTicks ?? 0);
 
 
-            var path = FFMpegManager.Instance.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass");
+            var path = FFMpegManager.Instance.GetSubtitleCachePath(state.MediaPath, state.SubtitleStream, offset, ".ass");
 
 
             if (performConversion)
             if (performConversion)
             {
             {
                 InputType type;
                 InputType type;
 
 
-                var inputPath = MediaEncoderHelpers.GetInputArgument(video, null, out type);
+                var inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.IsRemote, state.VideoType, state.IsoType, null, state.PlayableStreamFileNames, out type);
 
 
                 try
                 try
                 {
                 {
@@ -445,7 +442,7 @@ namespace MediaBrowser.Api.Playback
 
 
                     Directory.CreateDirectory(parentPath);
                     Directory.CreateDirectory(parentPath);
 
 
-                    var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, subtitleStream.Index, offset, path, CancellationToken.None);
+                    var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, state.SubtitleStream.Index, offset, path, CancellationToken.None);
 
 
                     Task.WaitAll(task);
                     Task.WaitAll(task);
                 }
                 }
@@ -461,22 +458,16 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the converted ass path.
         /// Gets the converted ass path.
         /// </summary>
         /// </summary>
-        /// <param name="video">The video.</param>
+        /// <param name="mediaPath">The media path.</param>
         /// <param name="subtitleStream">The subtitle stream.</param>
         /// <param name="subtitleStream">The subtitle stream.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        private string GetConvertedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion)
+        private string GetConvertedAssPath(string mediaPath, MediaStream subtitleStream, long? startTimeTicks, bool performConversion)
         {
         {
-            // If it's already ass, no conversion neccessary
-            //if (string.Equals(Path.GetExtension(subtitleStream.Path), ".ass", StringComparison.OrdinalIgnoreCase))
-            //{
-            //    return subtitleStream.Path;
-            //}
-
             var offset = TimeSpan.FromTicks(startTimeTicks ?? 0);
             var offset = TimeSpan.FromTicks(startTimeTicks ?? 0);
 
 
-            var path = FFMpegManager.Instance.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass");
+            var path = FFMpegManager.Instance.GetSubtitleCachePath(mediaPath, subtitleStream, offset, ".ass");
 
 
             if (performConversion)
             if (performConversion)
             {
             {
@@ -524,25 +515,15 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the probe size argument.
         /// Gets the probe size argument.
         /// </summary>
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="mediaPath">The media path.</param>
+        /// <param name="isVideo">if set to <c>true</c> [is video].</param>
+        /// <param name="videoType">Type of the video.</param>
+        /// <param name="isoType">Type of the iso.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetProbeSizeArgument(BaseItem item)
+        protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType)
         {
         {
-            var type = InputType.AudioFile;
-
-            if (item is Audio)
-            {
-                type = MediaEncoderHelpers.GetInputType(item.Path, null, null);
-            }
-            else
-            {
-                var video = item as Video;
-
-                if (video != null)
-                {
-                    type = MediaEncoderHelpers.GetInputType(item.Path, video.VideoType, video.IsoType);
-                }
-            }
+            var type = !isVideo ? MediaEncoderHelpers.GetInputType(mediaPath, null, null) :
+                MediaEncoderHelpers.GetInputType(mediaPath, videoType, isoType);
 
 
             return MediaEncoder.GetProbeSizeArgument(type);
             return MediaEncoder.GetProbeSizeArgument(type);
         }
         }
@@ -652,22 +633,19 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the input argument.
         /// Gets the input argument.
         /// </summary>
         /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="isoMount">The iso mount.</param>
+        /// <param name="state">The state.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetInputArgument(BaseItem item, IIsoMount isoMount)
+        protected string GetInputArgument(StreamState state)
         {
         {
             var type = InputType.AudioFile;
             var type = InputType.AudioFile;
 
 
-            var inputPath = new[] { item.Path };
-
-            var video = item as Video;
+            var inputPath = new[] { state.MediaPath };
 
 
-            if (video != null)
+            if (state.IsInputVideo)
             {
             {
-                if (!(video.VideoType == VideoType.Iso && isoMount == null))
+                if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
                 {
                 {
-                    inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
+                    inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.IsRemote, state.VideoType, state.IsoType, state.IsoMount, state.PlayableStreamFileNames, out type);
                 }
                 }
             }
             }
 
 
@@ -686,11 +664,9 @@ namespace MediaBrowser.Api.Playback
 
 
             Directory.CreateDirectory(parentPath);
             Directory.CreateDirectory(parentPath);
 
 
-            var video = state.Item as Video;
-
-            if (video != null && video.VideoType == VideoType.Iso && video.IsoType.HasValue && IsoManager.CanMount(video.Path))
+            if (state.IsInputVideo && state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
             {
             {
-                state.IsoMount = await IsoManager.Mount(video.Path, CancellationToken.None).ConfigureAwait(false);
+                state.IsoMount = await IsoManager.Mount(state.MediaPath, CancellationToken.None).ConfigureAwait(false);
             }
             }
 
 
             var process = new Process
             var process = new Process
@@ -715,7 +691,7 @@ namespace MediaBrowser.Api.Playback
                 EnableRaisingEvents = true
                 EnableRaisingEvents = true
             };
             };
 
 
-            ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks, state.Item.Path, state.Request.DeviceId);
+            ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, state.IsInputVideo, state.Request.StartTimeTicks, state.MediaPath, state.Request.DeviceId);
 
 
             Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
             Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
 
 
@@ -754,13 +730,13 @@ namespace MediaBrowser.Api.Playback
             }
             }
 
 
             // Allow a small amount of time to buffer a little
             // Allow a small amount of time to buffer a little
-            if (state.Item is Video)
+            if (state.IsInputVideo)
             {
             {
                 await Task.Delay(500).ConfigureAwait(false);
                 await Task.Delay(500).ConfigureAwait(false);
             }
             }
 
 
             // This is arbitrary, but add a little buffer time when internet streaming
             // This is arbitrary, but add a little buffer time when internet streaming
-            if (state.Item.LocationType == LocationType.Remote)
+            if (state.IsRemote)
             {
             {
                 await Task.Delay(4000).ConfigureAwait(false);
                 await Task.Delay(4000).ConfigureAwait(false);
             }
             }
@@ -787,11 +763,11 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the user agent param.
         /// Gets the user agent param.
         /// </summary>
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="path">The path.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetUserAgentParam(BaseItem item)
+        protected string GetUserAgentParam(string path)
         {
         {
-            var useragent = GetUserAgent(item);
+            var useragent = GetUserAgent(path);
 
 
             if (!string.IsNullOrEmpty(useragent))
             if (!string.IsNullOrEmpty(useragent))
             {
             {
@@ -804,11 +780,11 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// <summary>
         /// Gets the user agent.
         /// Gets the user agent.
         /// </summary>
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="path">The path.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        protected string GetUserAgent(BaseItem item)
+        protected string GetUserAgent(string path)
         {
         {
-            if (item.Path.IndexOf("apple.com", StringComparison.OrdinalIgnoreCase) != -1)
+            if (path.IndexOf("apple.com", StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
                 return "QuickTime/7.7.4";
                 return "QuickTime/7.7.4";
             }
             }
@@ -852,8 +828,6 @@ namespace MediaBrowser.Api.Playback
         {
         {
             var item = DtoService.GetItemByDtoId(request.Id);
             var item = DtoService.GetItemByDtoId(request.Id);
 
 
-            var media = (IHasMediaStreams)item;
-
             var url = Request.PathInfo;
             var url = Request.PathInfo;
 
 
             if (!request.AudioCodec.HasValue)
             if (!request.AudioCodec.HasValue)
@@ -863,11 +837,25 @@ namespace MediaBrowser.Api.Playback
 
 
             var state = new StreamState
             var state = new StreamState
             {
             {
-                Item = item,
                 Request = request,
                 Request = request,
-                Url = url
+                RequestedUrl = url,
+                MediaPath = item.Path,
+                IsRemote = item.LocationType == LocationType.Remote
             };
             };
 
 
+            var video = item as Video;
+
+            if (video != null)
+            {
+                state.IsInputVideo = true;
+                state.VideoType = video.VideoType;
+                state.IsoType = video.IsoType;
+
+                state.PlayableStreamFileNames = video.PlayableStreamFileNames == null
+                    ? new List<string>()
+                    : video.PlayableStreamFileNames.ToList();
+            }
+
             var videoRequest = request as VideoStreamRequest;
             var videoRequest = request as VideoStreamRequest;
 
 
             var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery
             var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery

+ 4 - 5
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -4,7 +4,6 @@ using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -247,7 +246,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
         {
         {
-            var probeSize = GetProbeSizeArgument(state.Item);
+            var probeSize = GetProbeSizeArgument(state.MediaPath, state.IsInputVideo, state.VideoType, state.IsoType);
 
 
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
 
 
@@ -262,9 +261,9 @@ namespace MediaBrowser.Api.Playback.Hls
             var args = string.Format("{0}{1} {2} {3} -i {4}{5} -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{10}\"",
             var args = string.Format("{0}{1} {2} {3} -i {4}{5} -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{10}\"",
                 itsOffset,
                 itsOffset,
                 probeSize,
                 probeSize,
-                GetUserAgentParam(state.Item),
+                GetUserAgentParam(state.MediaPath),
                 GetFastSeekCommandLineParameter(state.Request),
                 GetFastSeekCommandLineParameter(state.Request),
-                GetInputArgument(state.Item, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(state.Request),
                 GetSlowSeekCommandLineParameter(state.Request),
                 threads,
                 threads,
                 GetMapArgs(state),
                 GetMapArgs(state),
@@ -275,7 +274,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             if (hlsVideoRequest != null)
             if (hlsVideoRequest != null)
             {
             {
-                if (hlsVideoRequest.AppendBaselineStream && state.Item is Video)
+                if (hlsVideoRequest.AppendBaselineStream && state.IsInputVideo)
                 {
                 {
                     var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8");
                     var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8");
 
 

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

@@ -105,7 +105,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
 
             return string.Format("{0} -i {1}{2} -threads {3}{4} {5} -id3v2_version 3 -write_id3v1 1 \"{6}\"",
             return string.Format("{0} -i {1}{2} -threads {3}{4} {5} -id3v2_version 3 -write_id3v1 1 \"{6}\"",
                 GetFastSeekCommandLineParameter(request),
                 GetFastSeekCommandLineParameter(request),
-                GetInputArgument(state.Item, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(request),
                 GetSlowSeekCommandLineParameter(request),
                 threads,
                 threads,
                 vn,
                 vn,

+ 10 - 64
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -1,16 +1,12 @@
-using MediaBrowser.Api.Images;
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -51,9 +47,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Try to infer based on the desired video codec
             // Try to infer based on the desired video codec
             if (videoRequest != null && videoRequest.VideoCodec.HasValue)
             if (videoRequest != null && videoRequest.VideoCodec.HasValue)
             {
             {
-                var video = state.Item as Video;
-
-                if (video != null)
+                if (state.IsInputVideo)
                 {
                 {
                     switch (videoRequest.VideoCodec.Value)
                     switch (videoRequest.VideoCodec.Value)
                     {
                     {
@@ -72,9 +66,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Try to infer based on the desired audio codec
             // Try to infer based on the desired audio codec
             if (state.Request.AudioCodec.HasValue)
             if (state.Request.AudioCodec.HasValue)
             {
             {
-                var audio = state.Item as Audio;
-
-                if (audio != null)
+                if (!state.IsInputVideo)
                 {
                 {
                     switch (state.Request.AudioCodec.Value)
                     switch (state.Request.AudioCodec.Value)
                     {
                     {
@@ -188,16 +180,11 @@ namespace MediaBrowser.Api.Playback.Progressive
         {
         {
             var state = GetState(request);
             var state = GetState(request);
 
 
-            if (request.AlbumArt)
-            {
-                return GetAlbumArtResponse(state);
-            }
-
             var responseHeaders = new Dictionary<string, string>();
             var responseHeaders = new Dictionary<string, string>();
 
 
-            if (request.Static && state.Item.LocationType == LocationType.Remote)
+            if (request.Static && state.IsRemote)
             {
             {
-                return GetStaticRemoteStreamResult(state.Item, responseHeaders, isHeadRequest).Result;
+                return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result;
             }
             }
 
 
             var outputPath = GetOutputFilePath(state);
             var outputPath = GetOutputFilePath(state);
@@ -210,7 +197,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
 
             if (request.Static)
             if (request.Static)
             {
             {
-                return ResultFactory.GetStaticFileResult(Request, state.Item.Path, FileShare.Read, responseHeaders, isHeadRequest);
+                return ResultFactory.GetStaticFileResult(Request, state.MediaPath, FileShare.Read, responseHeaders, isHeadRequest);
             }
             }
 
 
             if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
             if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
@@ -224,19 +211,19 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <summary>
         /// <summary>
         /// Gets the static remote stream result.
         /// Gets the static remote stream result.
         /// </summary>
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="mediaPath">The media path.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
         /// <returns>Task{System.Object}.</returns>
         /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetStaticRemoteStreamResult(BaseItem item, Dictionary<string, string> responseHeaders, bool isHeadRequest)
+        private async Task<object> GetStaticRemoteStreamResult(string mediaPath, Dictionary<string, string> responseHeaders, bool isHeadRequest)
         {
         {
             responseHeaders["Accept-Ranges"] = "none";
             responseHeaders["Accept-Ranges"] = "none";
 
 
             var httpClient = new HttpClient();
             var httpClient = new HttpClient();
 
 
-            using (var message = new HttpRequestMessage(HttpMethod.Get, item.Path))
+            using (var message = new HttpRequestMessage(HttpMethod.Get, mediaPath))
             {
             {
-                var useragent = GetUserAgent(item);
+                var useragent = GetUserAgent(mediaPath);
 
 
                 if (!string.IsNullOrEmpty(useragent))
                 if (!string.IsNullOrEmpty(useragent))
                 {
                 {
@@ -272,47 +259,6 @@ namespace MediaBrowser.Api.Playback.Progressive
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Gets the album art response.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.Object.</returns>
-        private object GetAlbumArtResponse(StreamState state)
-        {
-            var request = new GetItemImage
-            {
-                MaxWidth = 800,
-                MaxHeight = 800,
-                Type = ImageType.Primary,
-                Id = state.Item.Id.ToString()
-            };
-
-            // Try and find some image to return
-            if (!state.Item.HasImage(ImageType.Primary))
-            {
-                if (state.Item.HasImage(ImageType.Backdrop))
-                {
-                    request.Type = ImageType.Backdrop;
-                }
-                else if (state.Item.HasImage(ImageType.Thumb))
-                {
-                    request.Type = ImageType.Thumb;
-                }
-                else if (state.Item.HasImage(ImageType.Logo))
-                {
-                    request.Type = ImageType.Logo;
-                }
-            }
-
-            return new ImageService(UserManager, LibraryManager, ServerConfigurationManager.ApplicationPaths, null, ItemRepository, DtoService, ImageProcessor, null)
-            {
-                Logger = Logger,
-                Request = Request,
-                ResultFactory = ResultFactory
-
-            }.Get(request);
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the stream result.
         /// Gets the stream result.
         /// </summary>
         /// </summary>

+ 3 - 6
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -89,9 +88,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
         {
         {
-            var video = (Video)state.Item;
-
-            var probeSize = GetProbeSizeArgument(state.Item);
+            var probeSize = GetProbeSizeArgument(state.MediaPath, state.IsInputVideo, state.VideoType, state.IsoType);
 
 
             // Get the output codec name
             // Get the output codec name
             var videoCodec = GetVideoCodec(state.VideoRequest);
             var videoCodec = GetVideoCodec(state.VideoRequest);
@@ -108,9 +105,9 @@ namespace MediaBrowser.Api.Playback.Progressive
 
 
             return string.Format("{0} {1} {2} -i {3}{4}{5} {6} {7} -threads {8} {9}{10} \"{11}\"",
             return string.Format("{0} {1} {2} -i {3}{4}{5} {6} {7} -threads {8} {9}{10} \"{11}\"",
                 probeSize,
                 probeSize,
-                GetUserAgentParam(state.Item),
+                GetUserAgentParam(state.MediaPath),
                 GetFastSeekCommandLineParameter(state.Request),
                 GetFastSeekCommandLineParameter(state.Request),
-                GetInputArgument(video, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(state.Request),
                 GetSlowSeekCommandLineParameter(state.Request),
                 keyFrame,
                 keyFrame,
                 GetMapArgs(state),
                 GetMapArgs(state),

+ 16 - 7
MediaBrowser.Api/Playback/StreamState.cs

@@ -1,14 +1,13 @@
-using System.IO;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
+using System.Collections.Generic;
+using System.IO;
 
 
 namespace MediaBrowser.Api.Playback
 namespace MediaBrowser.Api.Playback
 {
 {
     public class StreamState
     public class StreamState
     {
     {
-        public string Url { get; set; }
+        public string RequestedUrl { get; set; }
 
 
         public StreamRequest Request { get; set; }
         public StreamRequest Request { get; set; }
 
 
@@ -29,12 +28,22 @@ namespace MediaBrowser.Api.Playback
 
 
         public MediaStream SubtitleStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
 
 
-        public BaseItem Item { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the iso mount.
         /// Gets or sets the iso mount.
         /// </summary>
         /// </summary>
         /// <value>The iso mount.</value>
         /// <value>The iso mount.</value>
         public IIsoMount IsoMount { get; set; }
         public IIsoMount IsoMount { get; set; }
+
+        public string MediaPath { get; set; }
+
+        public bool IsRemote { get; set; }
+
+        public bool IsInputVideo { get; set; }
+
+        public VideoType VideoType { get; set; }
+
+        public IsoType? IsoType { get; set; }
+
+        public List<string> PlayableStreamFileNames { get; set; }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Api/SearchService.cs

@@ -153,7 +153,7 @@ namespace MediaBrowser.Api
 
 
             if (item.HasImage(ImageType.Primary))
             if (item.HasImage(ImageType.Primary))
             {
             {
-                result.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, item.GetImage(ImageType.Primary));
+                result.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, item.GetImagePath(ImageType.Primary));
             }
             }
 
 
             var episode = item as Episode;
             var episode = item as Episode;

+ 42 - 0
MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs

@@ -216,6 +216,48 @@ namespace MediaBrowser.Common.Implementations.IO
 
 
             return new FileStream(path, mode, access, share);
             return new FileStream(path, mode, access, share);
         }
         }
+
+        /// <summary>
+        /// Swaps the files.
+        /// </summary>
+        /// <param name="file1">The file1.</param>
+        /// <param name="file2">The file2.</param>
+        public void SwapFiles(string file1, string file2)
+        {
+            var temp1 = Path.GetTempFileName();
+            var temp2 = Path.GetTempFileName();
+
+            // Copying over will fail against hidden files
+            RemoveHiddenAttribute(file1);
+            RemoveHiddenAttribute(file2);
+
+            File.Copy(file1, temp1, true);
+            File.Copy(file2, temp2, true);
+
+            File.Copy(temp1, file2, true);
+            File.Copy(temp2, file1, true);
+
+            File.Delete(temp1);
+            File.Delete(temp2);
+        }
+
+        /// <summary>
+        /// Removes the hidden attribute.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        private void RemoveHiddenAttribute(string path)
+        {
+            var currentFile = new FileInfo(path);
+
+            // This will fail if the file is hidden
+            if (currentFile.Exists)
+            {
+                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                {
+                    currentFile.Attributes &= ~FileAttributes.Hidden;
+                }
+            }
+        }
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 7 - 0
MediaBrowser.Common/IO/IFileSystem.cs

@@ -74,5 +74,12 @@ namespace MediaBrowser.Common.IO
         /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
         /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
         /// <returns>FileStream.</returns>
         /// <returns>FileStream.</returns>
         FileStream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share, bool isAsync = false);
         FileStream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share, bool isAsync = false);
+
+        /// <summary>
+        /// Swaps the files.
+        /// </summary>
+        /// <param name="file1">The file1.</param>
+        /// <param name="file2">The file2.</param>
+        void SwapFiles(string file1, string file2);
     }
     }
 }
 }

+ 4 - 4
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <returns>IEnumerable{IImageEnhancer}.</returns>
         /// <returns>IEnumerable{IImageEnhancer}.</returns>
-        IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType);
+        IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType);
 
 
         /// <summary>
         /// <summary>
         /// Gets the image cache tag.
         /// Gets the image cache tag.
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imagePath">The image path.</param>
         /// <param name="imagePath">The image path.</param>
         /// <returns>Guid.</returns>
         /// <returns>Guid.</returns>
-        Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath);
+        Guid GetImageCacheTag(IHasImages item, ImageType imageType, string imagePath);
 
 
         /// <summary>
         /// <summary>
         /// Gets the image cache tag.
         /// Gets the image cache tag.
@@ -67,7 +67,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="dateModified">The date modified.</param>
         /// <param name="dateModified">The date modified.</param>
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <returns>Guid.</returns>
         /// <returns>Guid.</returns>
-        Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified,
+        Guid GetImageCacheTag(IHasImages item, ImageType imageType, string originalImagePath, DateTime dateModified,
                               List<IImageEnhancer> imageEnhancers);
                               List<IImageEnhancer> imageEnhancers);
 
 
         /// <summary>
         /// <summary>
@@ -85,6 +85,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <returns>Task{System.String}.</returns>
         /// <returns>Task{System.String}.</returns>
-        Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex);
+        Task<string> GetEnhancedImage(IHasImages item, ImageType imageType, int imageIndex);
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs

@@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Drawing
 {
 {
     public class ImageProcessingOptions
     public class ImageProcessingOptions
     {
     {
-        public BaseItem Item { get; set; }
+        public IHasImages Item { get; set; }
 
 
         public ImageType ImageType { get; set; }
         public ImageType ImageType { get; set; }
 
 

+ 30 - 39
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// <summary>
     /// Class BaseItem
     /// Class BaseItem
     /// </summary>
     /// </summary>
-    public abstract class BaseItem : IHasProviderIds, ILibraryItem
+    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData
     {
     {
         protected BaseItem()
         protected BaseItem()
         {
         {
@@ -132,8 +132,8 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         [IgnoreDataMember]
         public string PrimaryImagePath
         public string PrimaryImagePath
         {
         {
-            get { return GetImage(ImageType.Primary); }
-            set { SetImage(ImageType.Primary, value); }
+            get { return this.GetImagePath(ImageType.Primary); }
+            set { this.SetImagePath(ImageType.Primary, value); }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1310,31 +1310,10 @@ namespace MediaBrowser.Controller.Entities
         /// Gets an image
         /// Gets an image
         /// </summary>
         /// </summary>
         /// <param name="type">The type.</param>
         /// <param name="type">The type.</param>
-        /// <returns>System.String.</returns>
-        /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
-        public string GetImage(ImageType type)
-        {
-            if (type == ImageType.Backdrop)
-            {
-                throw new ArgumentException("Backdrops should be accessed using Item.Backdrops");
-            }
-            if (type == ImageType.Screenshot)
-            {
-                throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
-            }
-
-            string val;
-            Images.TryGetValue(type, out val);
-            return val;
-        }
-
-        /// <summary>
-        /// Gets an image
-        /// </summary>
-        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
         /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
         /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
         /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
-        public bool HasImage(ImageType type)
+        public bool HasImage(ImageType type, int imageIndex)
         {
         {
             if (type == ImageType.Backdrop)
             if (type == ImageType.Backdrop)
             {
             {
@@ -1345,16 +1324,10 @@ namespace MediaBrowser.Controller.Entities
                 throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
                 throw new ArgumentException("Screenshots should be accessed using Item.Screenshots");
             }
             }
 
 
-            return !string.IsNullOrEmpty(GetImage(type));
+            return !string.IsNullOrEmpty(this.GetImagePath(type));
         }
         }
 
 
-        /// <summary>
-        /// Sets an image
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <param name="path">The path.</param>
-        /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
-        public void SetImage(ImageType type, string path)
+        public void SetImagePath(ImageType type, int index, string path)
         {
         {
             if (type == ImageType.Backdrop)
             if (type == ImageType.Backdrop)
             {
             {
@@ -1423,10 +1396,10 @@ namespace MediaBrowser.Controller.Entities
             else
             else
             {
             {
                 // Delete the source file
                 // Delete the source file
-                DeleteImagePath(GetImage(type));
+                DeleteImagePath(this.GetImagePath(type));
 
 
                 // Remove it from the item
                 // Remove it from the item
-                SetImage(type, null);
+                this.SetImagePath(type, null);
             }
             }
 
 
             // Refresh metadata
             // Refresh metadata
@@ -1597,13 +1570,13 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             if (imageType == ImageType.Backdrop)
             if (imageType == ImageType.Backdrop)
             {
             {
-                return BackdropImagePaths[imageIndex];
+                return BackdropImagePaths.Count > imageIndex ? BackdropImagePaths[imageIndex] : null;
             }
             }
 
 
             if (imageType == ImageType.Screenshot)
             if (imageType == ImageType.Screenshot)
             {
             {
                 var hasScreenshots = (IHasScreenshots)this;
                 var hasScreenshots = (IHasScreenshots)this;
-                return hasScreenshots.ScreenshotImagePaths[imageIndex];
+                return hasScreenshots.ScreenshotImagePaths.Count > imageIndex ? hasScreenshots.ScreenshotImagePaths[imageIndex] : null;
             }
             }
 
 
             if (imageType == ImageType.Chapter)
             if (imageType == ImageType.Chapter)
@@ -1611,7 +1584,9 @@ namespace MediaBrowser.Controller.Entities
                 return ItemRepository.GetChapter(Id, imageIndex).ImagePath;
                 return ItemRepository.GetChapter(Id, imageIndex).ImagePath;
             }
             }
 
 
-            return GetImage(imageType);
+            string val;
+            Images.TryGetValue(imageType, out val);
+            return val;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1658,5 +1633,21 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             return new[] { Path };
             return new[] { Path };
         }
         }
+
+        public Task SwapImages(ImageType type, int index1, int index2)
+        {
+            if (type != ImageType.Screenshot && type != ImageType.Backdrop)
+            {
+                throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+            }
+
+            var file1 = GetImagePath(type, index1);
+            var file2 = GetImagePath(type, index2);
+
+            FileSystem.SwapFiles(file1, file2);
+
+            // Directory watchers should repeat this, but do a quick refresh first
+            return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false);
+        }
     }
     }
 }
 }

+ 97 - 0
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -0,0 +1,97 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasImages
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        string Path { get; }
+        
+        /// <summary>
+        /// Gets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        Guid Id { get; }
+
+        /// <summary>
+        /// Gets the image path.
+        /// </summary>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns>System.String.</returns>
+        string GetImagePath(ImageType imageType, int imageIndex);
+
+        /// <summary>
+        /// Gets the image date modified.
+        /// </summary>
+        /// <param name="imagePath">The image path.</param>
+        /// <returns>DateTime.</returns>
+        DateTime GetImageDateModified(string imagePath);
+
+        /// <summary>
+        /// Sets the image.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="index">The index.</param>
+        /// <param name="path">The path.</param>
+        void SetImagePath(ImageType type, int index, string path);
+
+        /// <summary>
+        /// Determines whether the specified type has image.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
+        bool HasImage(ImageType type, int imageIndex);
+
+        /// <summary>
+        /// Swaps the images.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="index1">The index1.</param>
+        /// <param name="index2">The index2.</param>
+        /// <returns>Task.</returns>
+        Task SwapImages(ImageType type, int index1, int index2);
+    }
+
+    public static class HasImagesExtensions
+    {
+        /// <summary>
+        /// Gets the image path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <returns>System.String.</returns>
+        public static string GetImagePath(this IHasImages item, ImageType imageType)
+        {
+            return item.GetImagePath(imageType, 0);
+        }
+
+        public static bool HasImage(this IHasImages item, ImageType imageType)
+        {
+            return item.HasImage(imageType, 0);
+        }
+        
+        /// <summary>
+        /// Sets the image path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="path">The path.</param>
+        public static void SetImagePath(this IHasImages item, ImageType imageType, string path)
+        {
+            item.SetImagePath(imageType, 0, path);
+        }
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Entities/IHasUserData.cs

@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Interface IHasUserData
+    /// </summary>
+    public interface IHasUserData
+    {
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        string GetUserDataKey();
+    }
+}

+ 3 - 1
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -183,7 +183,9 @@ namespace MediaBrowser.Controller.Entities.TV
                 episodes = episodes.Where(i => !i.IsVirtualUnaired);
                 episodes = episodes.Where(i => !i.IsVirtualUnaired);
             }
             }
 
 
-            return LibraryManager.Sort(episodes, user, new[] { ItemSortBy.AiredEpisodeOrder }, SortOrder.Ascending)
+            var sortBy = seasonNumber == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
+
+            return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending)
                 .Cast<Episode>();
                 .Cast<Episode>();
         }
         }
 
 

+ 1 - 1
MediaBrowser.Controller/Library/IUserDataManager.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="reason">The reason.</param>
         /// <param name="reason">The reason.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        Task SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+        Task SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets the user data.
         /// Gets the user data.

+ 1 - 1
MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs

@@ -37,6 +37,6 @@ namespace MediaBrowser.Controller.Library
         /// Gets or sets the item.
         /// Gets or sets the item.
         /// </summary>
         /// </summary>
         /// <value>The item.</value>
         /// <value>The item.</value>
-        public BaseItem Item { get; set; }
+        public IHasUserData Item { get; set; }
     }
     }
 }
 }

+ 0 - 75
MediaBrowser.Controller/LiveTv/Channel.cs

@@ -1,75 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.LiveTv;
-using System;
-using System.Collections.Generic;
-using System.Runtime.Serialization;
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class Channel : BaseItem, IItemByName
-    {
-        public Channel()
-        {
-            UserItemCountList = new List<ItemByNameCounts>();
-        }
-
-        /// <summary>
-        /// Gets the user data key.
-        /// </summary>
-        /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
-        {
-            return "Channel-" + Name;
-        }
-
-        [IgnoreDataMember]
-        public List<ItemByNameCounts> UserItemCountList { get; set; }
-
-        /// <summary>
-        /// Gets or sets the number.
-        /// </summary>
-        /// <value>The number.</value>
-        public string ChannelNumber { get; set; }
-
-        /// <summary>
-        /// Get or sets the Id.
-        /// </summary>
-        /// <value>The id of the channel.</value>
-        public string ChannelId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the service.
-        /// </summary>
-        /// <value>The name of the service.</value>
-        public string ServiceName { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the channel.
-        /// </summary>
-        /// <value>The type of the channel.</value>
-        public ChannelType ChannelType { get; set; }
-
-        public bool? HasProviderImage { get; set; }
-        
-        protected override string CreateSortName()
-        {
-            double number = 0;
-
-            if (!string.IsNullOrEmpty(ChannelNumber))
-            {
-                double.TryParse(ChannelNumber, out number);
-            }
-
-            return number.ToString("000-") + (Name ?? string.Empty);
-        }
-
-        public override string MediaType
-        {
-            get
-            {
-                return ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
-            }
-        }
-    }
-}

+ 9 - 3
MediaBrowser.Controller/LiveTv/ChannelInfo.cs

@@ -32,9 +32,15 @@ namespace MediaBrowser.Controller.LiveTv
         public ChannelType ChannelType { get; set; }
         public ChannelType ChannelType { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Set this value to true or false if it is known via channel info whether there is an image or not.
-        /// Leave it null if the only way to determine is by requesting the image and handling the failure.
+        /// Supply the image path if it can be accessed directly from the file system
         /// </summary>
         /// </summary>
-        public bool? HasImage { get; set; }
+        /// <value>The image path.</value>
+        public string ImagePath { get; set; }
+
+        /// <summary>
+        /// Supply the image url if it can be downloaded
+        /// </summary>
+        /// <value>The image URL.</value>
+        public string ImageUrl { get; set; }
     }
     }
 }
 }

+ 9 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -144,8 +144,16 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <param name="id">The identifier.</param>
         /// <returns>Channel.</returns>
         /// <returns>Channel.</returns>
-        Channel GetChannel(string id);
+        LiveTvChannel GetInternalChannel(string id);
 
 
+        /// <summary>
+        /// Gets the recording.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>LiveTvRecording.</returns>
+        Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken);
+        
         /// <summary>
         /// <summary>
         /// Gets the program.
         /// Gets the program.
         /// </summary>
         /// </summary>

+ 3 - 3
MediaBrowser.Controller/LiveTv/ILiveTvService.cs

@@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
         Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
         
         
         /// <summary>
         /// <summary>
-        /// Gets the channel image asynchronous.
+        /// Gets the channel image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to ChannelInfo
         /// </summary>
         /// </summary>
         /// <param name="channelId">The channel identifier.</param>
         /// <param name="channelId">The channel identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -87,7 +87,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task<ImageResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken);
         Task<ImageResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
-        /// Gets the recording image asynchronous.
+        /// Gets the recording image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to RecordingInfo
         /// </summary>
         /// </summary>
         /// <param name="recordingId">The recording identifier.</param>
         /// <param name="recordingId">The recording identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task<ImageResponseInfo> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken);
         Task<ImageResponseInfo> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
-        /// Gets the program image asynchronous.
+        /// Gets the program image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to ProgramInfo
         /// </summary>
         /// </summary>
         /// <param name="programId">The program identifier.</param>
         /// <param name="programId">The program identifier.</param>
         /// <param name="channelId">The channel identifier.</param>
         /// <param name="channelId">The channel identifier.</param>

+ 57 - 0
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs

@@ -0,0 +1,57 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class LiveTvChannel : BaseItem, IItemByName
+    {
+        public LiveTvChannel()
+        {
+            UserItemCountList = new List<ItemByNameCounts>();
+        }
+
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public override string GetUserDataKey()
+        {
+            return GetClientTypeName() + "-" + Name;
+        }
+
+        [IgnoreDataMember]
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
+
+        public ChannelInfo ChannelInfo { get; set; }
+
+        public string ServiceName { get; set; }
+
+        protected override string CreateSortName()
+        {
+            double number = 0;
+
+            if (!string.IsNullOrEmpty(ChannelInfo.Number))
+            {
+                double.TryParse(ChannelInfo.Number, out number);
+            }
+
+            return number.ToString("000-") + (Name ?? string.Empty);
+        }
+
+        public override string MediaType
+        {
+            get
+            {
+                return ChannelInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+            }
+        }
+
+        public override string GetClientTypeName()
+        {
+            return "Channel";
+        }
+    }
+}

+ 33 - 0
MediaBrowser.Controller/LiveTv/LiveTvProgram.cs

@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class LiveTvProgram : BaseItem
+    {
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public override string GetUserDataKey()
+        {
+            return GetClientTypeName() + "-" + Name;
+        }
+
+        public ProgramInfo ProgramInfo { get; set; }
+
+        public string ServiceName { get; set; }
+
+        public override string MediaType
+        {
+            get
+            {
+                return ProgramInfo.IsVideo ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
+            }
+        }
+
+        public override string GetClientTypeName()
+        {
+            return "Program";
+        }
+    }
+}

+ 43 - 0
MediaBrowser.Controller/LiveTv/LiveTvRecording.cs

@@ -0,0 +1,43 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class LiveTvRecording : BaseItem
+    {
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public override string GetUserDataKey()
+        {
+            return GetClientTypeName() + "-" + Name;
+        }
+
+        public RecordingInfo RecordingInfo { get; set; }
+
+        public string ServiceName { get; set; }
+
+        public override string MediaType
+        {
+            get
+            {
+                return RecordingInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+            }
+        }
+
+        public override LocationType LocationType
+        {
+            get
+            {
+                return LocationType.Remote;
+            }
+        }
+
+        public override string GetClientTypeName()
+        {
+            return "Recording";
+        }
+    }
+}

+ 16 - 4
MediaBrowser.Controller/LiveTv/ProgramInfo.cs

@@ -98,10 +98,16 @@ namespace MediaBrowser.Controller.LiveTv
         public string EpisodeTitle { get; set; }
         public string EpisodeTitle { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Set this value to true or false if it is known via program info whether there is an image or not.
-        /// Leave it null if the only way to determine is by requesting the image and handling the failure.
+        /// Supply the image path if it can be accessed directly from the file system
         /// </summary>
         /// </summary>
-        public bool? HasImage { get; set; }
+        /// <value>The image path.</value>
+        public string ImagePath { get; set; }
+
+        /// <summary>
+        /// Supply the image url if it can be downloaded
+        /// </summary>
+        /// <value>The image URL.</value>
+        public string ImageUrl { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether this instance is movie.
         /// Gets or sets a value indicating whether this instance is movie.
@@ -120,7 +126,13 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
         public bool IsSeries { get; set; }
         public bool IsSeries { get; set; }
-        
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is video.
+        /// </summary>
+        /// <value><c>true</c> if this instance is video; otherwise, <c>false</c>.</value>
+        public bool IsVideo { get; set; }
+
         public ProgramInfo()
         public ProgramInfo()
         {
         {
             Genres = new List<string>();
             Genres = new List<string>();

+ 9 - 3
MediaBrowser.Controller/LiveTv/RecordingInfo.cs

@@ -114,10 +114,16 @@ namespace MediaBrowser.Controller.LiveTv
         public float? CommunityRating { get; set; }
         public float? CommunityRating { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Set this value to true or false if it is known via recording info whether there is an image or not.
-        /// Leave it null if the only way to determine is by requesting the image and handling the failure.
+        /// Supply the image path if it can be accessed directly from the file system
         /// </summary>
         /// </summary>
-        public bool? HasImage { get; set; }
+        /// <value>The image path.</value>
+        public string ImagePath { get; set; }
+
+        /// <summary>
+        /// Supply the image url if it can be downloaded
+        /// </summary>
+        /// <value>The image URL.</value>
+        public string ImageUrl { get; set; }
 
 
         public RecordingInfo()
         public RecordingInfo()
         {
         {

+ 5 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -85,6 +85,7 @@
     <Compile Include="Entities\IHasAspectRatio.cs" />
     <Compile Include="Entities\IHasAspectRatio.cs" />
     <Compile Include="Entities\IHasBudget.cs" />
     <Compile Include="Entities\IHasBudget.cs" />
     <Compile Include="Entities\IHasCriticRating.cs" />
     <Compile Include="Entities\IHasCriticRating.cs" />
+    <Compile Include="Entities\IHasImages.cs" />
     <Compile Include="Entities\IHasLanguage.cs" />
     <Compile Include="Entities\IHasLanguage.cs" />
     <Compile Include="Entities\IHasMediaStreams.cs" />
     <Compile Include="Entities\IHasMediaStreams.cs" />
     <Compile Include="Entities\IHasProductionLocations.cs" />
     <Compile Include="Entities\IHasProductionLocations.cs" />
@@ -94,6 +95,7 @@
     <Compile Include="Entities\IHasTags.cs" />
     <Compile Include="Entities\IHasTags.cs" />
     <Compile Include="Entities\IHasThemeMedia.cs" />
     <Compile Include="Entities\IHasThemeMedia.cs" />
     <Compile Include="Entities\IHasTrailers.cs" />
     <Compile Include="Entities\IHasTrailers.cs" />
+    <Compile Include="Entities\IHasUserData.cs" />
     <Compile Include="Entities\IItemByName.cs" />
     <Compile Include="Entities\IItemByName.cs" />
     <Compile Include="Entities\ILibraryItem.cs" />
     <Compile Include="Entities\ILibraryItem.cs" />
     <Compile Include="Entities\ImageSourceInfo.cs" />
     <Compile Include="Entities\ImageSourceInfo.cs" />
@@ -106,11 +108,13 @@
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
-    <Compile Include="LiveTv\Channel.cs" />
+    <Compile Include="LiveTv\LiveTvChannel.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
     <Compile Include="LiveTv\ILiveTvService.cs" />
     <Compile Include="LiveTv\ILiveTvService.cs" />
     <Compile Include="LiveTv\ImageResponseInfo.cs" />
     <Compile Include="LiveTv\ImageResponseInfo.cs" />
+    <Compile Include="LiveTv\LiveTvProgram.cs" />
+    <Compile Include="LiveTv\LiveTvRecording.cs" />
     <Compile Include="LiveTv\ProgramInfo.cs" />
     <Compile Include="LiveTv\ProgramInfo.cs" />
     <Compile Include="LiveTv\RecordingInfo.cs" />
     <Compile Include="LiveTv\RecordingInfo.cs" />
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />

+ 8 - 18
MediaBrowser.Controller/MediaInfo/FFMpegManager.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Controller.MediaInfo
 
 
                         InputType type;
                         InputType type;
 
 
-                        var inputPath = MediaEncoderHelpers.GetInputArgument(video, null, out type);
+                        var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, false, video.VideoType, video.IsoType, null, video.PlayableStreamFileNames, out type);
 
 
                         try
                         try
                         {
                         {
@@ -233,33 +233,23 @@ namespace MediaBrowser.Controller.MediaInfo
         /// <summary>
         /// <summary>
         /// Gets the subtitle cache path.
         /// Gets the subtitle cache path.
         /// </summary>
         /// </summary>
-        /// <param name="input">The input.</param>
-        /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+        /// <param name="mediaPath">The media path.</param>
+        /// <param name="subtitleStream">The subtitle stream.</param>
         /// <param name="offset">The offset.</param>
         /// <param name="offset">The offset.</param>
         /// <param name="outputExtension">The output extension.</param>
         /// <param name="outputExtension">The output extension.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        public string GetSubtitleCachePath(Video input, int subtitleStreamIndex, TimeSpan? offset, string outputExtension)
+        public string GetSubtitleCachePath(string mediaPath, MediaStream subtitleStream, TimeSpan? offset, string outputExtension)
         {
         {
             var ticksParam = offset.HasValue ? "_" + offset.Value.Ticks : "";
             var ticksParam = offset.HasValue ? "_" + offset.Value.Ticks : "";
 
 
-            var stream = _itemRepo.GetMediaStreams(new MediaStreamQuery
+            if (subtitleStream.IsExternal)
             {
             {
-                ItemId = input.Id,
-                Index = subtitleStreamIndex
-
-            }).FirstOrDefault();
-
-            if (stream == null)
-            {
-                return null;
+                ticksParam += _fileSystem.GetLastWriteTimeUtc(subtitleStream.Path).Ticks;
             }
             }
 
 
-            if (stream.IsExternal)
-            {
-                ticksParam += _fileSystem.GetLastWriteTimeUtc(stream.Path).Ticks;
-            }
+            var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
 
 
-            var filename = (input.Id + "_" + subtitleStreamIndex.ToString(_usCulture) + "_" + input.DateModified.Ticks.ToString(_usCulture) + ticksParam).GetMD5() + outputExtension;
+            var filename = (mediaPath + "_" + subtitleStream.Index.ToString(_usCulture) + "_" + date.Ticks.ToString(_usCulture) + ticksParam).GetMD5() + outputExtension;
 
 
             var prefix = filename.Substring(0, 1);
             var prefix = filename.Substring(0, 1);
 
 

+ 29 - 11
MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs

@@ -1,5 +1,8 @@
-using MediaBrowser.Common.MediaInfo;
-using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 
 
@@ -13,43 +16,47 @@ namespace MediaBrowser.Controller.MediaInfo
         /// <summary>
         /// <summary>
         /// Gets the input argument.
         /// Gets the input argument.
         /// </summary>
         /// </summary>
-        /// <param name="video">The video.</param>
+        /// <param name="videoPath">The video path.</param>
+        /// <param name="isRemote">if set to <c>true</c> [is remote].</param>
+        /// <param name="videoType">Type of the video.</param>
+        /// <param name="isoType">Type of the iso.</param>
         /// <param name="isoMount">The iso mount.</param>
         /// <param name="isoMount">The iso mount.</param>
+        /// <param name="playableStreamFileNames">The playable stream file names.</param>
         /// <param name="type">The type.</param>
         /// <param name="type">The type.</param>
         /// <returns>System.String[][].</returns>
         /// <returns>System.String[][].</returns>
-        public static string[] GetInputArgument(Video video, IIsoMount isoMount, out InputType type)
+        public static string[] GetInputArgument(string videoPath, bool isRemote, VideoType videoType, IsoType? isoType, IIsoMount isoMount, IEnumerable<string> playableStreamFileNames, out InputType type)
         {
         {
-            var inputPath = isoMount == null ? new[] { video.Path } : new[] { isoMount.MountedPath };
+            var inputPath = isoMount == null ? new[] { videoPath } : new[] { isoMount.MountedPath };
 
 
             type = InputType.VideoFile;
             type = InputType.VideoFile;
 
 
-            switch (video.VideoType)
+            switch (videoType)
             {
             {
                 case VideoType.BluRay:
                 case VideoType.BluRay:
                     type = InputType.Bluray;
                     type = InputType.Bluray;
                     break;
                     break;
                 case VideoType.Dvd:
                 case VideoType.Dvd:
                     type = InputType.Dvd;
                     type = InputType.Dvd;
-                    inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray();
+                    inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray();
                     break;
                     break;
                 case VideoType.Iso:
                 case VideoType.Iso:
-                    if (video.IsoType.HasValue)
+                    if (isoType.HasValue)
                     {
                     {
-                        switch (video.IsoType.Value)
+                        switch (isoType.Value)
                         {
                         {
                             case IsoType.BluRay:
                             case IsoType.BluRay:
                                 type = InputType.Bluray;
                                 type = InputType.Bluray;
                                 break;
                                 break;
                             case IsoType.Dvd:
                             case IsoType.Dvd:
                                 type = InputType.Dvd;
                                 type = InputType.Dvd;
-                                inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray();
+                                inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray();
                                 break;
                                 break;
                         }
                         }
                     }
                     }
                     break;
                     break;
                 case VideoType.VideoFile:
                 case VideoType.VideoFile:
                     {
                     {
-                        if (video.LocationType == LocationType.Remote)
+                        if (isRemote)
                         {
                         {
                             type = InputType.Url;
                             type = InputType.Url;
                         }
                         }
@@ -60,6 +67,17 @@ namespace MediaBrowser.Controller.MediaInfo
             return inputPath;
             return inputPath;
         }
         }
 
 
+        public static List<string> GetPlayableStreamFiles(string rootPath, IEnumerable<string> filenames)
+        {
+            var allFiles = Directory
+                .EnumerateFiles(rootPath, "*", SearchOption.AllDirectories)
+                .ToList();
+
+            return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase)))
+                .Where(f => !string.IsNullOrEmpty(f))
+                .ToList();
+        }
+        
         /// <summary>
         /// <summary>
         /// Gets the type of the input.
         /// Gets the type of the input.
         /// </summary>
         /// </summary>

+ 4 - 4
MediaBrowser.Controller/Providers/IImageEnhancer.cs

@@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
         /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
-        bool Supports(BaseItem item, ImageType imageType);
+        bool Supports(IHasImages item, ImageType imageType);
 
 
         /// <summary>
         /// <summary>
         /// Gets the priority or order in which this enhancer should be run.
         /// Gets the priority or order in which this enhancer should be run.
@@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <returns>Cache key relating to the current state of this item and configuration</returns>
         /// <returns>Cache key relating to the current state of this item and configuration</returns>
-        string GetConfigurationCacheKey(BaseItem item, ImageType imageType);
+        string GetConfigurationCacheKey(IHasImages item, ImageType imageType);
 
 
         /// <summary>
         /// <summary>
         /// Gets the size of the enhanced image.
         /// Gets the size of the enhanced image.
@@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="originalImageSize">Size of the original image.</param>
         /// <param name="originalImageSize">Size of the original image.</param>
         /// <returns>ImageSize.</returns>
         /// <returns>ImageSize.</returns>
-        ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize);
+        ImageSize GetEnhancedImageSize(IHasImages item, ImageType imageType, int imageIndex, ImageSize originalImageSize);
 
 
         /// <summary>
         /// <summary>
         /// Enhances the image async.
         /// Enhances the image async.
@@ -49,6 +49,6 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <returns>Task{Image}.</returns>
         /// <returns>Task{Image}.</returns>
         /// <exception cref="System.ArgumentNullException"></exception>
         /// <exception cref="System.ArgumentNullException"></exception>
-        Task<Image> EnhanceImageAsync(BaseItem item, Image originalImage, ImageType imageType, int imageIndex);
+        Task<Image> EnhanceImageAsync(IHasImages item, Image originalImage, ImageType imageType, int imageIndex);
     }
     }
 }
 }

+ 2 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -252,8 +252,8 @@ namespace MediaBrowser.Model.Configuration
             EnableVideoImageExtraction = true;
             EnableVideoImageExtraction = true;
 
 
             EnableMovieChapterImageExtraction = true;
             EnableMovieChapterImageExtraction = true;
-            EnableEpisodeChapterImageExtraction = true;
-            EnableOtherVideoChapterImageExtraction = true;
+            EnableEpisodeChapterImageExtraction = false;
+            EnableOtherVideoChapterImageExtraction = false;
 
 
 #if (DEBUG)
 #if (DEBUG)
             EnableDeveloperTools = true;
             EnableDeveloperTools = true;

+ 14 - 0
MediaBrowser.Model/LiveTv/ProgramInfoDto.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {
@@ -108,6 +109,12 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The episode title.</value>
         /// <value>The episode title.</value>
         public string EpisodeTitle { get; set; }
         public string EpisodeTitle { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the image tags.
+        /// </summary>
+        /// <value>The image tags.</value>
+        public Dictionary<ImageType, Guid> ImageTags { get; set; }
+        
         /// <summary>
         /// <summary>
         /// Gets or sets the user data.
         /// Gets or sets the user data.
         /// </summary>
         /// </summary>
@@ -132,9 +139,16 @@ namespace MediaBrowser.Model.LiveTv
         /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
         /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
         public bool IsSeries { get; set; }
         public bool IsSeries { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public string Type { get; set; }
+
         public ProgramInfoDto()
         public ProgramInfoDto()
         {
         {
             Genres = new List<string>();
             Genres = new List<string>();
+            ImageTags = new Dictionary<ImageType, Guid>();
         }
         }
     }
     }
 
 

+ 14 - 0
MediaBrowser.Model/LiveTv/RecordingInfoDto.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {
@@ -136,15 +137,28 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The audio.</value>
         /// <value>The audio.</value>
         public ProgramAudio? Audio { get; set; }
         public ProgramAudio? Audio { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the image tags.
+        /// </summary>
+        /// <value>The image tags.</value>
+        public Dictionary<ImageType, Guid> ImageTags { get; set; }
+        
         /// <summary>
         /// <summary>
         /// Gets or sets the user data.
         /// Gets or sets the user data.
         /// </summary>
         /// </summary>
         /// <value>The user data.</value>
         /// <value>The user data.</value>
         public UserItemDataDto UserData { get; set; }
         public UserItemDataDto UserData { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public string Type { get; set; }
+
         public RecordingInfoDto()
         public RecordingInfoDto()
         {
         {
             Genres = new List<string>();
             Genres = new List<string>();
+            ImageTags = new Dictionary<ImageType, Guid>();
         }
         }
     }
     }
 }
 }

+ 6 - 0
MediaBrowser.Model/LiveTv/RecordingQuery.cs

@@ -16,6 +16,12 @@
         /// </summary>
         /// </summary>
         /// <value>The user identifier.</value>
         /// <value>The user identifier.</value>
         public string UserId { get; set; }
         public string UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        public string Id { get; set; }
     }
     }
 
 
     public class TimerQuery
     public class TimerQuery

+ 9 - 9
MediaBrowser.Providers/ImageFromMediaLocationProvider.cs

@@ -212,7 +212,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Logo, image.FullName);
+                item.SetImagePath(ImageType.Logo, image.FullName);
             }
             }
 
 
             // Clearart
             // Clearart
@@ -220,7 +220,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Art, image.FullName);
+                item.SetImagePath(ImageType.Art, image.FullName);
             }
             }
 
 
             // Disc
             // Disc
@@ -229,7 +229,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Disc, image.FullName);
+                item.SetImagePath(ImageType.Disc, image.FullName);
             }
             }
 
 
             // Box Image
             // Box Image
@@ -237,7 +237,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Box, image.FullName);
+                item.SetImagePath(ImageType.Box, image.FullName);
             }
             }
 
 
             // BoxRear Image
             // BoxRear Image
@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.BoxRear, image.FullName);
+                item.SetImagePath(ImageType.BoxRear, image.FullName);
             }
             }
 
 
             // Thumbnail Image
             // Thumbnail Image
@@ -253,7 +253,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Menu, image.FullName);
+                item.SetImagePath(ImageType.Menu, image.FullName);
             }
             }
 
 
             PopulateBanner(item, args);
             PopulateBanner(item, args);
@@ -311,7 +311,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Primary, image.FullName);
+                item.SetImagePath(ImageType.Primary, image.FullName);
             }
             }
         }
         }
 
 
@@ -339,7 +339,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Banner, image.FullName);
+                item.SetImagePath(ImageType.Banner, image.FullName);
             }
             }
         }
         }
 
 
@@ -367,7 +367,7 @@ namespace MediaBrowser.Providers
 
 
             if (image != null)
             if (image != null)
             {
             {
-                item.SetImage(ImageType.Thumb, image.FullName);
+                item.SetImagePath(ImageType.Thumb, image.FullName);
             }
             }
 
 
         }
         }

+ 2 - 2
MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Providers.LiveTv
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         public override bool Supports(BaseItem item)
         public override bool Supports(BaseItem item)
         {
         {
-            return item is Channel;
+            return item is LiveTvChannel;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.LiveTv
 
 
                 try
                 try
                 {
                 {
-                    new BaseItemXmlParser<Channel>(Logger).Fetch((Channel)item, path, cancellationToken);
+                    new BaseItemXmlParser<LiveTvChannel>(Logger).Fetch((LiveTvChannel)item, path, cancellationToken);
                 }
                 }
                 finally
                 finally
                 {
                 {

+ 1 - 1
MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs

@@ -115,7 +115,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             if (video != null)
             if (video != null)
             {
             {
-                inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
+                inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type);
             }
             }
 
 
             return await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false);
             return await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false);

+ 1 - 1
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -253,7 +253,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 InputType type;
                 InputType type;
 
 
-                var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
+                var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type);
 
 
                 await _mediaEncoder.ExtractImage(inputPath, type, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false);
                 await _mediaEncoder.ExtractImage(inputPath, type, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false);
 
 

+ 1 - 1
MediaBrowser.Providers/Savers/ChannelXmlSaver.cs

@@ -29,7 +29,7 @@ namespace MediaBrowser.Providers.Savers
             // If new metadata has been downloaded or metadata was manually edited, proceed
             // If new metadata has been downloaded or metadata was manually edited, proceed
             if ((wasMetadataEdited || wasMetadataDownloaded))
             if ((wasMetadataEdited || wasMetadataDownloaded))
             {
             {
-                return item is Channel;
+                return item is LiveTvChannel;
             }
             }
 
 
             return false;
             return false;

+ 7 - 7
MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs

@@ -594,7 +594,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imagePath">The image path.</param>
         /// <param name="imagePath">The image path.</param>
         /// <returns>Guid.</returns>
         /// <returns>Guid.</returns>
         /// <exception cref="System.ArgumentNullException">item</exception>
         /// <exception cref="System.ArgumentNullException">item</exception>
-        public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath)
+        public Guid GetImageCacheTag(IHasImages item, ImageType imageType, string imagePath)
         {
         {
             if (item == null)
             if (item == null)
             {
             {
@@ -623,7 +623,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <returns>Guid.</returns>
         /// <returns>Guid.</returns>
         /// <exception cref="System.ArgumentNullException">item</exception>
         /// <exception cref="System.ArgumentNullException">item</exception>
-        public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, List<IImageEnhancer> imageEnhancers)
+        public Guid GetImageCacheTag(IHasImages item, ImageType imageType, string originalImagePath, DateTime dateModified, List<IImageEnhancer> imageEnhancers)
         {
         {
             if (item == null)
             if (item == null)
             {
             {
@@ -660,7 +660,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <returns>Task{System.String}.</returns>
         /// <returns>Task{System.String}.</returns>
-        public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
+        public async Task<string> GetEnhancedImage(IHasImages item, ImageType imageType, int imageIndex)
         {
         {
             var enhancers = GetSupportedEnhancers(item, imageType).ToList();
             var enhancers = GetSupportedEnhancers(item, imageType).ToList();
 
 
@@ -673,7 +673,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return result.Item1;
             return result.Item1;
         }
         }
 
 
-        private async Task<Tuple<string, DateTime>> GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item,
+        private async Task<Tuple<string, DateTime>> GetEnhancedImage(string originalImagePath, DateTime dateModified, IHasImages item,
                                                     ImageType imageType, int imageIndex,
                                                     ImageType imageType, int imageIndex,
                                                     List<IImageEnhancer> enhancers)
                                                     List<IImageEnhancer> enhancers)
         {
         {
@@ -709,7 +709,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="supportedEnhancers">The supported enhancers.</param>
         /// <param name="supportedEnhancers">The supported enhancers.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         /// <exception cref="System.ArgumentNullException">originalImagePath</exception>
         /// <exception cref="System.ArgumentNullException">originalImagePath</exception>
-        private async Task<string> GetEnhancedImageInternal(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex, List<IImageEnhancer> supportedEnhancers)
+        private async Task<string> GetEnhancedImageInternal(string originalImagePath, DateTime dateModified, IHasImages item, ImageType imageType, int imageIndex, List<IImageEnhancer> supportedEnhancers)
         {
         {
             if (string.IsNullOrEmpty(originalImagePath))
             if (string.IsNullOrEmpty(originalImagePath))
             {
             {
@@ -782,7 +782,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <returns>Task{EnhancedImage}.</returns>
         /// <returns>Task{EnhancedImage}.</returns>
-        private async Task<Image> ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex)
+        private async Task<Image> ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, Image originalImage, IHasImages item, ImageType imageType, int imageIndex)
         {
         {
             var result = originalImage;
             var result = originalImage;
 
 
@@ -900,7 +900,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return Path.Combine(path, filename);
             return Path.Combine(path, filename);
         }
         }
 
 
-        public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
+        public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
         {
         {
             return ImageEnhancers.Where(i =>
             return ImageEnhancers.Where(i =>
             {
             {

+ 4 - 4
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -818,7 +818,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                 {
                     dto.ParentLogoItemId = GetDtoId(parentWithLogo);
                     dto.ParentLogoItemId = GetDtoId(parentWithLogo);
 
 
-                    dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo));
+                    dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImagePath(ImageType.Logo));
                 }
                 }
             }
             }
 
 
@@ -831,7 +831,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                 {
                     dto.ParentArtItemId = GetDtoId(parentWithImage);
                     dto.ParentArtItemId = GetDtoId(parentWithImage);
 
 
-                    dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art, parentWithImage.GetImage(ImageType.Art));
+                    dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art, parentWithImage.GetImagePath(ImageType.Art));
                 }
                 }
             }
             }
 
 
@@ -844,7 +844,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                 {
                     dto.ParentThumbItemId = GetDtoId(parentWithImage);
                     dto.ParentThumbItemId = GetDtoId(parentWithImage);
 
 
-                    dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb, parentWithImage.GetImage(ImageType.Thumb));
+                    dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb, parentWithImage.GetImagePath(ImageType.Thumb));
                 }
                 }
             }
             }
 
 
@@ -1037,7 +1037,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
                 if (series.HasImage(ImageType.Thumb))
                 if (series.HasImage(ImageType.Thumb))
                 {
                 {
-                    dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImage(ImageType.Thumb));
+                    dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImagePath(ImageType.Thumb));
                 }
                 }
 
 
                 var imagePath = series.PrimaryImagePath;
                 var imagePath = series.PrimaryImagePath;

+ 1 - 1
MediaBrowser.Server.Implementations/Library/UserDataManager.cs

@@ -49,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// userId
         /// userId
         /// or
         /// or
         /// key</exception>
         /// key</exception>
-        public async Task SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
+        public async Task SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
         {
         {
             if (userData == null)
             if (userData == null)
             {
             {

+ 55 - 22
MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -21,18 +22,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
+        private readonly IHttpClient _httpClient;
 
 
-        public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem)
+        public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient)
             : base(logManager, configurationManager)
             : base(logManager, configurationManager)
         {
         {
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
             _providerManager = providerManager;
             _providerManager = providerManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
+            _httpClient = httpClient;
         }
         }
 
 
         public override bool Supports(BaseItem item)
         public override bool Supports(BaseItem item)
         {
         {
-            return item is Channel;
+            return item is LiveTvChannel;
         }
         }
 
 
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
@@ -48,21 +51,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 return true;
                 return true;
             }
             }
 
 
-            var channel = (Channel)item;
-
-            if (channel.HasProviderImage ?? true)
+            try
             {
             {
-                try
-                {
-                    await DownloadImage(item, cancellationToken).ConfigureAwait(false);
-                }
-                catch (HttpException ex)
+                await DownloadImage((LiveTvChannel)item, cancellationToken).ConfigureAwait(false);
+            }
+            catch (HttpException ex)
+            {
+                // Don't fail the provider on a 404
+                if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
                 {
                 {
-                    // Don't fail the provider on a 404
-                    if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
-                    {
-                        throw;
-                    }
+                    throw;
                 }
                 }
             }
             }
 
 
@@ -70,20 +68,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return true;
             return true;
         }
         }
 
 
-        private async Task DownloadImage(BaseItem item, CancellationToken cancellationToken)
+        private async Task DownloadImage(LiveTvChannel item, CancellationToken cancellationToken)
         {
         {
-            var channel = (Channel)item;
+            var channelInfo = item.ChannelInfo;
 
 
-            var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, channel.ServiceName, StringComparison.OrdinalIgnoreCase));
+            Stream imageStream = null;
+            string contentType = null;
 
 
-            if (service != null)
+            if (!string.IsNullOrEmpty(channelInfo.ImagePath))
             {
             {
-                var response = await service.GetChannelImageAsync(channel.ChannelId, cancellationToken).ConfigureAwait(false);
+                contentType = "image/" + Path.GetExtension(channelInfo.ImagePath).ToLower();
+                imageStream = _fileSystem.GetFileStream(channelInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true);
+            }
+            else if (!string.IsNullOrEmpty(channelInfo.ImageUrl))
+            {
+                var options = new HttpRequestOptions
+                {
+                    CancellationToken = cancellationToken,
+                    Url = channelInfo.ImageUrl
+                };
+
+                var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
 
 
+                if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                {
+                    throw new InvalidOperationException("Provider did not return an image content type.");
+                }
+
+                imageStream = response.Content;
+                contentType = response.ContentType;
+            }
+            else
+            {
+                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase));
+
+                if (service != null)
+                {
+                    var response = await service.GetChannelImageAsync(channelInfo.Id, cancellationToken).ConfigureAwait(false);
+
+                    imageStream = response.Stream;
+                    contentType = response.MimeType;
+                }
+            }
+
+            if (imageStream != null)
+            {
                 // Dummy up the original url
                 // Dummy up the original url
-                var url = channel.ServiceName + channel.ChannelId;
+                var url = item.ServiceName + channelInfo.Id;
 
 
-                await _providerManager.SaveImage(channel, response.Stream, response.MimeType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
+                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
             }
             }
         }
         }
 
 

+ 81 - 33
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -135,11 +135,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return pattern;
             return pattern;
         }
         }
 
 
-        public RecordingInfoDto GetRecordingInfoDto(RecordingInfo info, ILiveTvService service, User user = null)
+        /// <summary>
+        /// Convert the provider 0-5 scale to our 0-10 scale
+        /// </summary>
+        /// <param name="val"></param>
+        /// <returns></returns>
+        private float? GetClientCommunityRating(float? val)
+        {
+            if (!val.HasValue)
+            {
+                return null;
+            }
+
+            return val.Value * 2;
+        }
+
+        public RecordingInfoDto GetRecordingInfoDto(LiveTvRecording recording, ILiveTvService service, User user = null)
         {
         {
+            var info = recording.RecordingInfo;
+
             var dto = new RecordingInfoDto
             var dto = new RecordingInfoDto
             {
             {
                 Id = GetInternalRecordingId(service.Name, info.Id).ToString("N"),
                 Id = GetInternalRecordingId(service.Name, info.Id).ToString("N"),
+                Type = recording.GetClientTypeName(),
                 ChannelName = info.ChannelName,
                 ChannelName = info.ChannelName,
                 Overview = info.Overview,
                 Overview = info.Overview,
                 EndDate = info.EndDate,
                 EndDate = info.EndDate,
@@ -154,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 EpisodeTitle = info.EpisodeTitle,
                 EpisodeTitle = info.EpisodeTitle,
                 ChannelType = info.ChannelType,
                 ChannelType = info.ChannelType,
                 MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video,
                 MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video,
-                CommunityRating = info.CommunityRating,
+                CommunityRating = GetClientCommunityRating(info.CommunityRating),
                 OfficialRating = info.OfficialRating,
                 OfficialRating = info.OfficialRating,
                 Audio = info.Audio,
                 Audio = info.Audio,
                 IsHD = info.IsHD,
                 IsHD = info.IsHD,
@@ -162,9 +180,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Url = info.Url
                 Url = info.Url
             };
             };
 
 
+            var imageTag = GetImageTag(recording);
+
+            if (imageTag.HasValue)
+            {
+                dto.ImageTags[ImageType.Primary] = imageTag.Value;
+            }
+
             if (user != null)
             if (user != null)
             {
             {
-                //dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey()));
+                dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, recording.GetUserDataKey()));
             }
             }
 
 
             var duration = info.EndDate - info.StartDate;
             var duration = info.EndDate - info.StartDate;
@@ -184,18 +209,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         /// <param name="info">The info.</param>
         /// <param name="info">The info.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         /// <returns>ChannelInfoDto.</returns>
         /// <returns>ChannelInfoDto.</returns>
-        public ChannelInfoDto GetChannelInfoDto(Channel info, User user = null)
+        public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, User user = null)
         {
         {
+            var channelInfo = info.ChannelInfo;
+
             var dto = new ChannelInfoDto
             var dto = new ChannelInfoDto
             {
             {
                 Name = info.Name,
                 Name = info.Name,
                 ServiceName = info.ServiceName,
                 ServiceName = info.ServiceName,
-                ChannelType = info.ChannelType,
-                Number = info.ChannelNumber,
-                Type = info.GetType().Name,
+                ChannelType = channelInfo.ChannelType,
+                Number = channelInfo.Number,
+                Type = info.GetClientTypeName(),
                 Id = info.Id.ToString("N"),
                 Id = info.Id.ToString("N"),
                 MediaType = info.MediaType,
                 MediaType = info.MediaType,
-                ExternalId = info.ChannelId
+                ExternalId = channelInfo.Id
             };
             };
 
 
             if (user != null)
             if (user != null)
@@ -203,7 +230,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey()));
                 dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey()));
             }
             }
 
 
-            var imageTag = GetLogoImageTag(info);
+            var imageTag = GetImageTag(info);
 
 
             if (imageTag.HasValue)
             if (imageTag.HasValue)
             {
             {
@@ -213,7 +240,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return dto;
             return dto;
         }
         }
 
 
-        public ProgramInfoDto GetProgramInfoDto(ProgramInfo program, Channel channel, User user = null)
+        public ProgramInfoDto GetProgramInfoDto(ProgramInfo program, LiveTvChannel channel, User user = null)
         {
         {
             var dto = new ProgramInfoDto
             var dto = new ProgramInfoDto
             {
             {
@@ -230,7 +257,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 IsHD = program.IsHD,
                 IsHD = program.IsHD,
                 OriginalAirDate = program.OriginalAirDate,
                 OriginalAirDate = program.OriginalAirDate,
                 Audio = program.Audio,
                 Audio = program.Audio,
-                CommunityRating = program.CommunityRating,
+                CommunityRating = GetClientCommunityRating(program.CommunityRating),
                 AspectRatio = program.AspectRatio,
                 AspectRatio = program.AspectRatio,
                 IsRepeat = program.IsRepeat,
                 IsRepeat = program.IsRepeat,
                 EpisodeTitle = program.EpisodeTitle,
                 EpisodeTitle = program.EpisodeTitle,
@@ -248,7 +275,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return dto;
             return dto;
         }
         }
 
 
-        private Guid? GetLogoImageTag(Channel info)
+        private Guid? GetImageTag(BaseItem info)
         {
         {
             var path = info.PrimaryImagePath;
             var path = info.PrimaryImagePath;
 
 
@@ -263,7 +290,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
-                _logger.ErrorException("Error getting channel image info for {0}", ex, info.Name);
+                _logger.ErrorException("Error getting image info for {0}", ex, info.Name);
             }
             }
 
 
             return null;
             return null;
@@ -273,7 +300,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
         {
             var name = serviceName + externalId + channelName;
             var name = serviceName + externalId + channelName;
 
 
-            return name.ToLower().GetMBId(typeof(Channel));
+            return name.ToLower().GetMBId(typeof(LiveTvChannel));
         }
         }
 
 
         public Guid GetInternalTimerId(string serviceName, string externalId)
         public Guid GetInternalTimerId(string serviceName, string externalId)
@@ -314,41 +341,53 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Name = dto.Name,
                 Name = dto.Name,
                 StartDate = dto.StartDate,
                 StartDate = dto.StartDate,
                 Status = dto.Status,
                 Status = dto.Status,
-                SeriesTimerId = dto.ExternalSeriesTimerId,
                 PrePaddingSeconds = dto.PrePaddingSeconds,
                 PrePaddingSeconds = dto.PrePaddingSeconds,
                 PostPaddingSeconds = dto.PostPaddingSeconds,
                 PostPaddingSeconds = dto.PostPaddingSeconds,
                 IsPostPaddingRequired = dto.IsPostPaddingRequired,
                 IsPostPaddingRequired = dto.IsPostPaddingRequired,
                 IsPrePaddingRequired = dto.IsPrePaddingRequired,
                 IsPrePaddingRequired = dto.IsPrePaddingRequired,
-                Priority = dto.Priority
+                Priority = dto.Priority,
+                SeriesTimerId = dto.ExternalSeriesTimerId,
+                ProgramId = dto.ExternalProgramId,
+                ChannelId = dto.ExternalChannelId,
+                Id = dto.ExternalId
             };
             };
 
 
             // Convert internal server id's to external tv provider id's
             // Convert internal server id's to external tv provider id's
-            if (!isNew && !string.IsNullOrEmpty(dto.Id))
+            if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id))
             {
             {
-                var timer = await liveTv.GetTimer(dto.Id, cancellationToken).ConfigureAwait(false);
+                var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false);
 
 
                 info.Id = timer.ExternalId;
                 info.Id = timer.ExternalId;
             }
             }
 
 
-            if (!string.IsNullOrEmpty(dto.SeriesTimerId))
+            if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId))
             {
             {
-                var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false);
+                var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false);
 
 
-                info.SeriesTimerId = timer.ExternalId;
+                if (channel != null)
+                {
+                    info.ChannelId = channel.ExternalId;
+                }
             }
             }
 
 
-            if (!string.IsNullOrEmpty(dto.ChannelId))
+            if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId))
             {
             {
-                var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false);
+                var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false);
 
 
-                info.ChannelId = channel.ExternalId;
+                if (program != null)
+                {
+                    info.ProgramId = program.ExternalId;
+                }
             }
             }
 
 
-            if (!string.IsNullOrEmpty(dto.ProgramId))
+            if (!string.IsNullOrEmpty(dto.SeriesTimerId) && string.IsNullOrEmpty(info.SeriesTimerId))
             {
             {
-                var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false);
+                var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false);
 
 
-                info.ProgramId = program.ExternalId;
+                if (timer != null)
+                {
+                    info.SeriesTimerId = timer.ExternalId;
+                }
             }
             }
 
 
             return info;
             return info;
@@ -371,29 +410,38 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Priority = dto.Priority,
                 Priority = dto.Priority,
                 RecordAnyChannel = dto.RecordAnyChannel,
                 RecordAnyChannel = dto.RecordAnyChannel,
                 RecordAnyTime = dto.RecordAnyTime,
                 RecordAnyTime = dto.RecordAnyTime,
-                RecordNewOnly = dto.RecordNewOnly
+                RecordNewOnly = dto.RecordNewOnly,
+                ProgramId = dto.ExternalProgramId,
+                ChannelId = dto.ExternalChannelId,
+                Id = dto.ExternalId
             };
             };
 
 
             // Convert internal server id's to external tv provider id's
             // Convert internal server id's to external tv provider id's
-            if (!isNew && !string.IsNullOrEmpty(dto.Id))
+            if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id))
             {
             {
                 var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false);
                 var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false);
 
 
                 info.Id = timer.ExternalId;
                 info.Id = timer.ExternalId;
             }
             }
 
 
-            if (!string.IsNullOrEmpty(dto.ChannelId))
+            if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId))
             {
             {
                 var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false);
                 var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false);
 
 
-                info.ChannelId = channel.ExternalId;
+                if (channel != null)
+                {
+                    info.ChannelId = channel.ExternalId;
+                }
             }
             }
 
 
-            if (!string.IsNullOrEmpty(dto.ProgramId))
+            if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId))
             {
             {
                 var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false);
                 var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false);
 
 
-                info.ProgramId = program.ExternalId;
+                if (program != null)
+                {
+                    info.ProgramId = program.ExternalId;
+                }
             }
             }
 
 
             return info;
             return info;

+ 85 - 29
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -36,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
         private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
         private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
 
 
-        private List<Channel> _channels = new List<Channel>();
+        private List<LiveTvChannel> _channels = new List<LiveTvChannel>();
         private List<ProgramInfoDto> _programs = new List<ProgramInfoDto>();
         private List<ProgramInfoDto> _programs = new List<ProgramInfoDto>();
 
 
         public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager)
         public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager)
@@ -77,18 +77,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
         {
             var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
             var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
 
 
-            IEnumerable<Channel> channels = _channels;
+            IEnumerable<LiveTvChannel> channels = _channels;
 
 
             if (user != null)
             if (user != null)
             {
             {
-                channels = channels.Where(i => i.IsParentalAllowed(user, _localization))
+                channels = channels
+                    .Where(i => i.IsParentalAllowed(user, _localization))
                     .OrderBy(i =>
                     .OrderBy(i =>
                     {
                     {
                         double number = 0;
                         double number = 0;
 
 
-                        if (!string.IsNullOrEmpty(i.ChannelNumber))
+                        if (!string.IsNullOrEmpty(i.ChannelInfo.Number))
                         {
                         {
-                            double.TryParse(i.ChannelNumber, out number);
+                            double.TryParse(i.ChannelInfo.Number, out number);
                         }
                         }
 
 
                         return number;
                         return number;
@@ -100,9 +101,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             {
             {
                 double number = 0;
                 double number = 0;
 
 
-                if (!string.IsNullOrEmpty(i.ChannelNumber))
+                if (!string.IsNullOrEmpty(i.ChannelInfo.Number))
                 {
                 {
-                    double.TryParse(i.ChannelNumber, out number);
+                    double.TryParse(i.ChannelInfo.Number, out number);
                 }
                 }
 
 
                 return number;
                 return number;
@@ -120,14 +121,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return Task.FromResult(result);
             return Task.FromResult(result);
         }
         }
 
 
-        public Channel GetChannel(string id)
+        public LiveTvChannel GetInternalChannel(string id)
         {
         {
             var guid = new Guid(id);
             var guid = new Guid(id);
 
 
             return _channels.FirstOrDefault(i => i.Id == guid);
             return _channels.FirstOrDefault(i => i.Id == guid);
         }
         }
 
 
-        private async Task<Channel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)
+        public async Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken)
+        {
+            var service = ActiveService;
+
+            var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
+
+            var recording = recordings.FirstOrDefault(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(id));
+
+            return await GetRecording(recording, service.Name, cancellationToken).ConfigureAwait(false);
+        }
+        
+        private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)
         {
         {
             var path = Path.Combine(_appPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(serviceName), _fileSystem.GetValidFilename(channelInfo.Name));
             var path = Path.Combine(_appPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(serviceName), _fileSystem.GetValidFilename(channelInfo.Name));
 
 
@@ -150,26 +162,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id, channelInfo.Name);
             var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id, channelInfo.Name);
 
 
-            var item = _itemRepo.RetrieveItem(id) as Channel;
+            var item = _itemRepo.RetrieveItem(id) as LiveTvChannel;
 
 
             if (item == null)
             if (item == null)
             {
             {
-                item = new Channel
+                item = new LiveTvChannel
                 {
                 {
                     Name = channelInfo.Name,
                     Name = channelInfo.Name,
                     Id = id,
                     Id = id,
                     DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo),
                     DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo),
                     DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo),
                     DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo),
-                    Path = path,
-                    ChannelId = channelInfo.Id,
-                    ChannelNumber = channelInfo.Number,
-                    ServiceName = serviceName,
-                    HasProviderImage = channelInfo.HasImage
+                    Path = path
                 };
                 };
 
 
                 isNew = true;
                 isNew = true;
             }
             }
 
 
+            item.ChannelInfo = channelInfo;
+            item.ServiceName = serviceName;
+
             // Set this now so we don't cause additional file system access during provider executions
             // Set this now so we don't cause additional file system access during provider executions
             item.ResetResolveArgs(fileInfo);
             item.ResetResolveArgs(fileInfo);
 
 
@@ -178,6 +189,35 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return item;
             return item;
         }
         }
 
 
+        private async Task<LiveTvRecording> GetRecording(RecordingInfo info, string serviceName, CancellationToken cancellationToken)
+        {
+            var isNew = false;
+
+            var id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id);
+
+            var item = _itemRepo.RetrieveItem(id) as LiveTvRecording;
+
+            if (item == null)
+            {
+                item = new LiveTvRecording
+                {
+                    Name = info.Name,
+                    Id = id,
+                    DateCreated = DateTime.UtcNow,
+                    DateModified = DateTime.UtcNow
+                };
+
+                isNew = true;
+            }
+
+            item.RecordingInfo = info;
+            item.ServiceName = serviceName;
+
+            await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false);
+
+            return item;
+        }
+
         public Task<ProgramInfoDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
         public Task<ProgramInfoDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
         {
         {
             var program = _programs.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
             var program = _programs.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
@@ -225,7 +265,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             var allChannels = await GetChannels(service, cancellationToken).ConfigureAwait(false);
             var allChannels = await GetChannels(service, cancellationToken).ConfigureAwait(false);
             var allChannelsList = allChannels.ToList();
             var allChannelsList = allChannels.ToList();
 
 
-            var list = new List<Channel>();
+            var list = new List<LiveTvChannel>();
             var programs = new List<ProgramInfoDto>();
             var programs = new List<ProgramInfoDto>();
 
 
             var numComplete = 0;
             var numComplete = 0;
@@ -271,26 +311,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
         public async Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken)
         public async Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken)
         {
         {
-            var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
+            var service = ActiveService;
 
 
-            var list = new List<RecordingInfoDto>();
+            var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
 
 
-            if (ActiveService != null)
-            {
-                var recordings = await ActiveService.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
+            var list = new List<RecordingInfo>();
 
 
-                var dtos = recordings.Select(i => _tvDtoService.GetRecordingInfoDto(i, ActiveService, user));
+            var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
+            list.AddRange(recordings);
 
 
-                list.AddRange(dtos);
+            if (!string.IsNullOrEmpty(query.ChannelId))
+            {
+                list = list
+                    .Where(i => _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId, i.ChannelName) == new Guid(query.ChannelId))
+                    .ToList();
             }
             }
 
 
-            if (!string.IsNullOrEmpty(query.ChannelId))
+            if (!string.IsNullOrEmpty(query.Id))
             {
             {
-                list = list.Where(i => string.Equals(i.ChannelId, query.ChannelId))
+                list = list
+                    .Where(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(query.Id))
                     .ToList();
                     .ToList();
             }
             }
 
 
-            var returnArray = list.OrderByDescending(i => i.StartDate)
+            var entities = await GetEntities(list, service.Name, cancellationToken).ConfigureAwait(false);
+
+            var returnArray = entities
+                .Select(i => _tvDtoService.GetRecordingInfoDto(i, ActiveService, user))
+                .OrderByDescending(i => i.StartDate)
                 .ToArray();
                 .ToArray();
 
 
             return new QueryResult<RecordingInfoDto>
             return new QueryResult<RecordingInfoDto>
@@ -300,6 +348,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             };
             };
         }
         }
 
 
+        private Task<LiveTvRecording[]> GetEntities(IEnumerable<RecordingInfo> recordings, string serviceName, CancellationToken cancellationToken)
+        {
+            var tasks = recordings.Select(i => GetRecording(i, serviceName, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
         private IEnumerable<ILiveTvService> GetServices(string serviceName, string channelId)
         private IEnumerable<ILiveTvService> GetServices(string serviceName, string channelId)
         {
         {
             IEnumerable<ILiveTvService> services = _services;
             IEnumerable<ILiveTvService> services = _services;
@@ -404,11 +459,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
         {
             var results = await GetRecordings(new RecordingQuery
             var results = await GetRecordings(new RecordingQuery
             {
             {
-                UserId = user == null ? null : user.Id.ToString("N")
+                UserId = user == null ? null : user.Id.ToString("N"),
+                Id = id
 
 
             }, cancellationToken).ConfigureAwait(false);
             }, cancellationToken).ConfigureAwait(false);
 
 
-            return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture));
+            return results.Items.FirstOrDefault();
         }
         }
 
 
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
         public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)

+ 136 - 0
MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs

@@ -0,0 +1,136 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv
+{
+    public class ProgramImageProvider : BaseMetadataProvider
+    {
+        private readonly ILiveTvManager _liveTvManager;
+        private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
+        private readonly IHttpClient _httpClient;
+
+        public ProgramImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient)
+            : base(logManager, configurationManager)
+        {
+            _liveTvManager = liveTvManager;
+            _providerManager = providerManager;
+            _fileSystem = fileSystem;
+            _httpClient = httpClient;
+        }
+
+        public override bool Supports(BaseItem item)
+        {
+            return item is LiveTvProgram;
+        }
+
+        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        {
+            return !item.HasImage(ImageType.Primary);
+        }
+
+        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
+        {
+            if (item.HasImage(ImageType.Primary))
+            {
+                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
+                return true;
+            }
+
+            try
+            {
+                await DownloadImage((LiveTvProgram)item, cancellationToken).ConfigureAwait(false);
+            }
+            catch (HttpException ex)
+            {
+                // Don't fail the provider on a 404
+                if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
+                {
+                    throw;
+                }
+            }
+
+            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
+            return true;
+        }
+
+        private async Task DownloadImage(LiveTvProgram item, CancellationToken cancellationToken)
+        {
+            var programInfo = item.ProgramInfo;
+
+            Stream imageStream = null;
+            string contentType = null;
+
+            if (!string.IsNullOrEmpty(programInfo.ImagePath))
+            {
+                contentType = "image/" + Path.GetExtension(programInfo.ImagePath).ToLower();
+                imageStream = _fileSystem.GetFileStream(programInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true);
+            }
+            else if (!string.IsNullOrEmpty(programInfo.ImageUrl))
+            {
+                var options = new HttpRequestOptions
+                {
+                    CancellationToken = cancellationToken,
+                    Url = programInfo.ImageUrl
+                };
+
+                var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
+
+                if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                {
+                    throw new InvalidOperationException("Provider did not return an image content type.");
+                }
+
+                imageStream = response.Content;
+                contentType = response.ContentType;
+            }
+            else
+            {
+                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase));
+
+                if (service != null)
+                {
+                    var response = await service.GetProgramImageAsync(programInfo.Id, programInfo.ChannelId, cancellationToken).ConfigureAwait(false);
+
+                    imageStream = response.Stream;
+                    contentType = response.MimeType;
+                }
+            }
+
+            if (imageStream != null)
+            {
+                // Dummy up the original url
+                var url = item.ServiceName + programInfo.Id;
+
+                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        public override MetadataProviderPriority Priority
+        {
+            get { return MetadataProviderPriority.Second; }
+        }
+
+        public override ItemUpdateType ItemUpdateType
+        {
+            get
+            {
+                return ItemUpdateType.ImageUpdate;
+            }
+        }
+    }
+}

+ 136 - 0
MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs

@@ -0,0 +1,136 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+
+namespace MediaBrowser.Server.Implementations.LiveTv
+{
+    public class RecordingImageProvider : BaseMetadataProvider
+    {
+        private readonly ILiveTvManager _liveTvManager;
+        private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
+        private readonly IHttpClient _httpClient;
+
+        public RecordingImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient)
+            : base(logManager, configurationManager)
+        {
+            _liveTvManager = liveTvManager;
+            _providerManager = providerManager;
+            _fileSystem = fileSystem;
+            _httpClient = httpClient;
+        }
+
+        public override bool Supports(BaseItem item)
+        {
+            return item is LiveTvRecording;
+        }
+
+        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        {
+            return !item.HasImage(ImageType.Primary);
+        }
+
+        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
+        {
+            if (item.HasImage(ImageType.Primary))
+            {
+                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
+                return true;
+            }
+
+            try
+            {
+                await DownloadImage((LiveTvRecording)item, cancellationToken).ConfigureAwait(false);
+            }
+            catch (HttpException ex)
+            {
+                // Don't fail the provider on a 404
+                if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
+                {
+                    throw;
+                }
+            }
+
+            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
+            return true;
+        }
+
+        private async Task DownloadImage(LiveTvRecording item, CancellationToken cancellationToken)
+        {
+            var recordingInfo = item.RecordingInfo;
+
+            Stream imageStream = null;
+            string contentType = null;
+
+            if (!string.IsNullOrEmpty(recordingInfo.ImagePath))
+            {
+                contentType = "image/" + Path.GetExtension(recordingInfo.ImagePath).ToLower();
+                imageStream = _fileSystem.GetFileStream(recordingInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true);
+            }
+            else if (!string.IsNullOrEmpty(recordingInfo.ImageUrl))
+            {
+                var options = new HttpRequestOptions
+                {
+                    CancellationToken = cancellationToken,
+                    Url = recordingInfo.ImageUrl
+                };
+
+                var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
+
+                if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                {
+                    throw new InvalidOperationException("Provider did not return an image content type.");
+                }
+
+                imageStream = response.Content;
+                contentType = response.ContentType;
+            }
+            else
+            {
+                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase));
+
+                if (service != null)
+                {
+                    var response = await service.GetRecordingImageAsync(recordingInfo.Id, cancellationToken).ConfigureAwait(false);
+
+                    imageStream = response.Stream;
+                    contentType = response.MimeType;
+                }
+            }
+
+            if (imageStream != null)
+            {
+                // Dummy up the original url
+                var url = item.ServiceName + recordingInfo.Id;
+
+                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        public override MetadataProviderPriority Priority
+        {
+            get { return MetadataProviderPriority.Second; }
+        }
+
+        public override ItemUpdateType ItemUpdateType
+        {
+            get
+            {
+                return ItemUpdateType.ImageUpdate;
+            }
+        }
+    }
+}

+ 2 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -153,6 +153,8 @@
     <Compile Include="LiveTv\ChannelImageProvider.cs" />
     <Compile Include="LiveTv\ChannelImageProvider.cs" />
     <Compile Include="LiveTv\LiveTvDtoService.cs" />
     <Compile Include="LiveTv\LiveTvDtoService.cs" />
     <Compile Include="LiveTv\LiveTvManager.cs" />
     <Compile Include="LiveTv\LiveTvManager.cs" />
+    <Compile Include="LiveTv\ProgramImageProvider.cs" />
+    <Compile Include="LiveTv\RecordingImageProvider.cs" />
     <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
     <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
     <Compile Include="Localization\LocalizationManager.cs" />
     <Compile Include="Localization\LocalizationManager.cs" />
     <Compile Include="MediaEncoder\MediaEncoder.cs" />
     <Compile Include="MediaEncoder\MediaEncoder.cs" />

+ 8 - 24
MediaBrowser.Server.Implementations/Providers/ImageSaver.cs

@@ -118,6 +118,8 @@ namespace MediaBrowser.Server.Implementations.Providers
                 imageIndex = hasScreenshots.ScreenshotImagePaths.Count;
                 imageIndex = hasScreenshots.ScreenshotImagePaths.Count;
             }
             }
 
 
+            var index = imageIndex ?? 0;
+
             var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
             var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
 
 
             // If there are more than one output paths, the stream will need to be seekable
             // If there are more than one output paths, the stream will need to be seekable
@@ -132,7 +134,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                 source = memoryStream;
                 source = memoryStream;
             }
             }
 
 
-            var currentPath = GetCurrentImagePath(item, type, imageIndex);
+            var currentPath = GetCurrentImagePath(item, type, index);
 
 
             using (source)
             using (source)
             {
             {
@@ -152,7 +154,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                 }
                 }
             }
             }
 
 
-            // Set the path into the BaseItem
+            // Set the path into the item
             SetImagePath(item, type, imageIndex, paths[0], sourceUrl);
             SetImagePath(item, type, imageIndex, paths[0], sourceUrl);
 
 
             // Delete the current path
             // Delete the current path
@@ -257,27 +259,9 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// or
         /// or
         /// imageIndex
         /// imageIndex
         /// </exception>
         /// </exception>
-        private string GetCurrentImagePath(BaseItem item, ImageType type, int? imageIndex)
+        private string GetCurrentImagePath(IHasImages item, ImageType type, int imageIndex)
         {
         {
-            switch (type)
-            {
-                case ImageType.Screenshot:
-
-                    var hasScreenshots = (IHasScreenshots)item;
-                    if (!imageIndex.HasValue)
-                    {
-                        throw new ArgumentNullException("imageIndex");
-                    }
-                    return hasScreenshots.ScreenshotImagePaths.Count > imageIndex.Value ? hasScreenshots.ScreenshotImagePaths[imageIndex.Value] : null;
-                case ImageType.Backdrop:
-                    if (!imageIndex.HasValue)
-                    {
-                        throw new ArgumentNullException("imageIndex");
-                    }
-                    return item.BackdropImagePaths.Count > imageIndex.Value ? item.BackdropImagePaths[imageIndex.Value] : null;
-                default:
-                    return item.GetImage(type);
-            }
+            return item.GetImagePath(type, imageIndex);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -336,7 +320,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                     }
                     }
                     break;
                     break;
                 default:
                 default:
-                    item.SetImage(type, path);
+                    item.SetImagePath(type, path);
                     break;
                     break;
             }
             }
         }
         }
@@ -593,7 +577,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="imageFilename">The image filename.</param>
         /// <param name="imageFilename">The image filename.</param>
         /// <param name="extension">The extension.</param>
         /// <param name="extension">The extension.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        private string GetSavePathForItemInMixedFolder(BaseItem item, ImageType type, string imageFilename, string extension)
+        private string GetSavePathForItemInMixedFolder(IHasImages item, ImageType type, string imageFilename, string extension)
         {
         {
             if (type == ImageType.Primary)
             if (type == ImageType.Primary)
             {
             {

+ 1 - 1
MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs

@@ -96,7 +96,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
             // Limit to video files to reduce changes of ffmpeg crash dialog
             // Limit to video files to reduce changes of ffmpeg crash dialog
             foreach (var item in newItems
             foreach (var item in newItems
                 .Where(i => i.LocationType == LocationType.FileSystem && i.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(i.PrimaryImagePath) && i.DefaultVideoStreamIndex.HasValue)
                 .Where(i => i.LocationType == LocationType.FileSystem && i.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(i.PrimaryImagePath) && i.DefaultVideoStreamIndex.HasValue)
-                .Take(2))
+                .Take(1))
             {
             {
                 try
                 try
                 {
                 {

+ 4 - 4
MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs

@@ -242,19 +242,19 @@ namespace MediaBrowser.ServerApplication
                                        }
                                        }
                                        if (item.HasImage(ImageType.Banner))
                                        if (item.HasImage(ImageType.Banner))
                                        {
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Banner), "Banner"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Banner), "Banner"));
                                        }
                                        }
                                        if (item.HasImage(ImageType.Logo))
                                        if (item.HasImage(ImageType.Logo))
                                        {
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Logo), "Logo"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Logo), "Logo"));
                                        }
                                        }
                                        if (item.HasImage(ImageType.Art))
                                        if (item.HasImage(ImageType.Art))
                                        {
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Art), "Art"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Art), "Art"));
                                        }
                                        }
                                        if (item.HasImage(ImageType.Thumb))
                                        if (item.HasImage(ImageType.Thumb))
                                        {
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Thumb), "Thumb"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Thumb), "Thumb"));
                                        }
                                        }
                                        previews.AddRange(
                                        previews.AddRange(
                                            item.BackdropImagePaths.Select(
                                            item.BackdropImagePaths.Select(

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>MediaBrowser.Common.Internal</title>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.278" />
+            <dependency id="MediaBrowser.Common" version="3.0.281" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.0" />
             <dependency id="SimpleInjector" version="2.4.0" />
             <dependency id="sharpcompress" version="0.10.2" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common</id>
         <id>MediaBrowser.Common</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>MediaBrowser.Common</title>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Server.Core</id>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>Media Browser.Server.Core</title>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.278" />
+            <dependency id="MediaBrowser.Common" version="3.0.281" />
         </dependencies>
         </dependencies>
     </metadata>
     </metadata>
     <files>
     <files>