2
0
Эх сурвалжийг харах

added IHasImages and IHasUserData

Luke Pulverenti 11 жил өмнө
parent
commit
cd859ac2e6
59 өөрчлөгдсөн 1290 нэмэгдсэн , 650 устгасан
  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.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 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.Drawing;
@@ -19,8 +19,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using ServiceStack.Text.Controller;
-using ServiceStack.Web;
 
 namespace MediaBrowser.Api.Images
 {
@@ -39,18 +37,6 @@ namespace MediaBrowser.Api.Images
         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("/Genres/{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")]
         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>
     /// Class UpdateItemImageIndex
     /// </summary>
@@ -270,19 +243,6 @@ namespace MediaBrowser.Api.Images
         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>
     /// Class PostUserImage
     /// </summary>
@@ -358,38 +318,13 @@ namespace MediaBrowser.Api.Images
         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>
     /// Class ImageService
     /// </summary>
     public class ImageService : BaseApiService
     {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
         private readonly IUserManager _userManager;
 
-        /// <summary>
-        /// The _library manager
-        /// </summary>
         private readonly ILibraryManager _libraryManager;
 
         private readonly IApplicationPaths _appPaths;
@@ -400,12 +335,11 @@ namespace MediaBrowser.Api.Images
         private readonly IDtoService _dtoService;
         private readonly IImageProcessor _imageProcessor;
 
-        private readonly ILiveTvManager _liveTv;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageService" /> class.
         /// </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;
             _libraryManager = libraryManager;
@@ -414,7 +348,6 @@ namespace MediaBrowser.Api.Images
             _itemRepo = itemRepo;
             _dtoService = dtoService;
             _imageProcessor = imageProcessor;
-            _liveTv = liveTv;
         }
 
         /// <summary>
@@ -431,15 +364,6 @@ namespace MediaBrowser.Api.Images
             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)
         {
             var result = GetItemByNameImageInfos(request);
@@ -540,7 +464,7 @@ namespace MediaBrowser.Api.Images
             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
             {
@@ -567,13 +491,6 @@ namespace MediaBrowser.Api.Images
             }
         }
 
-        public object Get(GetChannelImage request)
-        {
-            var item = _liveTv.GetChannel(request.Id);
-
-            return GetImage(request, item);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -659,20 +576,6 @@ namespace MediaBrowser.Api.Images
             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>
         /// Deletes the specified request.
         /// </summary>
@@ -699,15 +602,6 @@ namespace MediaBrowser.Api.Images
             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>
         /// Deletes the specified request.
         /// </summary>
@@ -762,71 +656,9 @@ namespace MediaBrowser.Api.Images
         /// <param name="newIndex">The new index.</param>
         /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception>
-        private Task UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex)
+        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>
@@ -837,7 +669,7 @@ namespace MediaBrowser.Api.Images
         /// <returns>System.Object.</returns>
         /// <exception cref="ResourceNotFoundException">
         /// </exception>
-        private object GetImage(ImageRequest request, BaseItem item)
+        public object GetImage(ImageRequest request, IHasImages item)
         {
             var imagePath = GetImagePath(request, item);
 
@@ -926,7 +758,7 @@ namespace MediaBrowser.Api.Images
         /// <param name="request">The request.</param>
         /// <param name="item">The item.</param>
         /// <returns>System.String.</returns>
-        private string GetImagePath(ImageRequest request, BaseItem item)
+        private string GetImagePath(ImageRequest request, IHasImages item)
         {
             var index = request.Index ?? 0;
 
@@ -941,7 +773,7 @@ namespace MediaBrowser.Api.Images
         /// <param name="imageType">Type of the image.</param>
         /// <param name="mimeType">Type of the MIME.</param>
         /// <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))
             {

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

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

+ 1 - 1
MediaBrowser.Api/ItemUpdateService.cs

@@ -146,7 +146,7 @@ namespace MediaBrowser.Api
 
         private async Task UpdateItem(UpdateChannel request)
         {
-            var item = _liveTv.GetChannel(request.Id);
+            var item = _liveTv.GetInternalChannel(request.Id);
 
             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\LibraryService.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
+    <Compile Include="LiveTv\LiveTvImageService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LocalizationService.cs" />
     <Compile Include="MoviesService.cs" />

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

@@ -1,11 +1,9 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
@@ -110,7 +108,7 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         protected virtual string GetOutputFileExtension(StreamState state)
         {
-            return Path.GetExtension(state.Url);
+            return Path.GetExtension(state.RequestedUrl);
         }
 
         /// <summary>
@@ -187,7 +185,7 @@ namespace MediaBrowser.Api.Playback
         {
             var args = string.Empty;
 
-            if (state.Item.LocationType == LocationType.Remote)
+            if (state.IsRemote)
             {
                 return string.Empty;
             }
@@ -308,7 +306,7 @@ namespace MediaBrowser.Api.Playback
 
             return args.Trim();
         }
-        
+
         /// <summary>
         /// If we're going to put a fixed size on the command line, this will calculate it
         /// </summary>
@@ -331,7 +329,7 @@ namespace MediaBrowser.Api.Playback
                     string.Equals(state.SubtitleStream.Codec, "ass", 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>
         /// Gets the text subtitle param.
         /// </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="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <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))
             {
@@ -422,22 +420,21 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the extracted ass path.
         /// </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="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <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 path = FFMpegManager.Instance.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass");
+            var path = FFMpegManager.Instance.GetSubtitleCachePath(state.MediaPath, state.SubtitleStream, offset, ".ass");
 
             if (performConversion)
             {
                 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
                 {
@@ -445,7 +442,7 @@ namespace MediaBrowser.Api.Playback
 
                     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);
                 }
@@ -461,22 +458,16 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the converted ass path.
         /// </summary>
-        /// <param name="video">The video.</param>
+        /// <param name="mediaPath">The media path.</param>
         /// <param name="subtitleStream">The subtitle stream.</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
         /// <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 path = FFMpegManager.Instance.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass");
+            var path = FFMpegManager.Instance.GetSubtitleCachePath(mediaPath, subtitleStream, offset, ".ass");
 
             if (performConversion)
             {
@@ -524,25 +515,15 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the probe size argument.
         /// </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>
-        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);
         }
@@ -652,22 +633,19 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the input argument.
         /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="isoMount">The iso mount.</param>
+        /// <param name="state">The state.</param>
         /// <returns>System.String.</returns>
-        protected string GetInputArgument(BaseItem item, IIsoMount isoMount)
+        protected string GetInputArgument(StreamState state)
         {
             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);
 
-            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
@@ -715,7 +691,7 @@ namespace MediaBrowser.Api.Playback
                 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);
 
@@ -754,13 +730,13 @@ namespace MediaBrowser.Api.Playback
             }
 
             // Allow a small amount of time to buffer a little
-            if (state.Item is Video)
+            if (state.IsInputVideo)
             {
                 await Task.Delay(500).ConfigureAwait(false);
             }
 
             // 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);
             }
@@ -787,11 +763,11 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the user agent param.
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="path">The path.</param>
         /// <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))
             {
@@ -804,11 +780,11 @@ namespace MediaBrowser.Api.Playback
         /// <summary>
         /// Gets the user agent.
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="path">The path.</param>
         /// <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";
             }
@@ -852,8 +828,6 @@ namespace MediaBrowser.Api.Playback
         {
             var item = DtoService.GetItemByDtoId(request.Id);
 
-            var media = (IHasMediaStreams)item;
-
             var url = Request.PathInfo;
 
             if (!request.AudioCodec.HasValue)
@@ -863,11 +837,25 @@ namespace MediaBrowser.Api.Playback
 
             var state = new StreamState
             {
-                Item = item,
                 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 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.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
@@ -247,7 +246,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.String.</returns>
         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;
 
@@ -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}\"",
                 itsOffset,
                 probeSize,
-                GetUserAgentParam(state.Item),
+                GetUserAgentParam(state.MediaPath),
                 GetFastSeekCommandLineParameter(state.Request),
-                GetInputArgument(state.Item, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(state.Request),
                 threads,
                 GetMapArgs(state),
@@ -275,7 +274,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             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");
 

+ 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}\"",
                 GetFastSeekCommandLineParameter(request),
-                GetInputArgument(state.Item, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(request),
                 threads,
                 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.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.IO;
@@ -51,9 +47,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Try to infer based on the desired video codec
             if (videoRequest != null && videoRequest.VideoCodec.HasValue)
             {
-                var video = state.Item as Video;
-
-                if (video != null)
+                if (state.IsInputVideo)
                 {
                     switch (videoRequest.VideoCodec.Value)
                     {
@@ -72,9 +66,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Try to infer based on the desired audio codec
             if (state.Request.AudioCodec.HasValue)
             {
-                var audio = state.Item as Audio;
-
-                if (audio != null)
+                if (!state.IsInputVideo)
                 {
                     switch (state.Request.AudioCodec.Value)
                     {
@@ -188,16 +180,11 @@ namespace MediaBrowser.Api.Playback.Progressive
         {
             var state = GetState(request);
 
-            if (request.AlbumArt)
-            {
-                return GetAlbumArtResponse(state);
-            }
-
             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);
@@ -210,7 +197,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             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))
@@ -224,19 +211,19 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <summary>
         /// Gets the static remote stream result.
         /// </summary>
-        /// <param name="item">The item.</param>
+        /// <param name="mediaPath">The media path.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
         /// <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";
 
             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))
                 {
@@ -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>
         /// Gets the stream result.
         /// </summary>

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

@@ -3,7 +3,6 @@ using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
@@ -89,9 +88,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <returns>System.String.</returns>
         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
             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}\"",
                 probeSize,
-                GetUserAgentParam(state.Item),
+                GetUserAgentParam(state.MediaPath),
                 GetFastSeekCommandLineParameter(state.Request),
-                GetInputArgument(video, state.IsoMount),
+                GetInputArgument(state),
                 GetSlowSeekCommandLineParameter(state.Request),
                 keyFrame,
                 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 System.Collections.Generic;
+using System.IO;
 
 namespace MediaBrowser.Api.Playback
 {
     public class StreamState
     {
-        public string Url { get; set; }
+        public string RequestedUrl { get; set; }
 
         public StreamRequest Request { get; set; }
 
@@ -29,12 +28,22 @@ namespace MediaBrowser.Api.Playback
 
         public MediaStream SubtitleStream { get; set; }
 
-        public BaseItem Item { get; set; }
-
         /// <summary>
         /// Gets or sets the iso mount.
         /// </summary>
         /// <value>The iso mount.</value>
         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))
             {
-                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;

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

@@ -216,6 +216,48 @@ namespace MediaBrowser.Common.Implementations.IO
 
             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>

+ 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>
         /// <returns>FileStream.</returns>
         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="imageType">Type of the image.</param>
         /// <returns>IEnumerable{IImageEnhancer}.</returns>
-        IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType);
+        IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType);
 
         /// <summary>
         /// Gets the image cache tag.
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imagePath">The image path.</param>
         /// <returns>Guid.</returns>
-        Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath);
+        Guid GetImageCacheTag(IHasImages item, ImageType imageType, string imagePath);
 
         /// <summary>
         /// Gets the image cache tag.
@@ -67,7 +67,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="dateModified">The date modified.</param>
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <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);
 
         /// <summary>
@@ -85,6 +85,6 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <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 BaseItem Item { get; set; }
+        public IHasImages Item { get; set; }
 
         public ImageType ImageType { get; set; }
 

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

@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// Class BaseItem
     /// </summary>
-    public abstract class BaseItem : IHasProviderIds, ILibraryItem
+    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData
     {
         protected BaseItem()
         {
@@ -132,8 +132,8 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         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>
@@ -1310,31 +1310,10 @@ namespace MediaBrowser.Controller.Entities
         /// Gets an image
         /// </summary>
         /// <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>
         /// <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)
             {
@@ -1345,16 +1324,10 @@ namespace MediaBrowser.Controller.Entities
                 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)
             {
@@ -1423,10 +1396,10 @@ namespace MediaBrowser.Controller.Entities
             else
             {
                 // Delete the source file
-                DeleteImagePath(GetImage(type));
+                DeleteImagePath(this.GetImagePath(type));
 
                 // Remove it from the item
-                SetImage(type, null);
+                this.SetImagePath(type, null);
             }
 
             // Refresh metadata
@@ -1597,13 +1570,13 @@ namespace MediaBrowser.Controller.Entities
         {
             if (imageType == ImageType.Backdrop)
             {
-                return BackdropImagePaths[imageIndex];
+                return BackdropImagePaths.Count > imageIndex ? BackdropImagePaths[imageIndex] : null;
             }
 
             if (imageType == ImageType.Screenshot)
             {
                 var hasScreenshots = (IHasScreenshots)this;
-                return hasScreenshots.ScreenshotImagePaths[imageIndex];
+                return hasScreenshots.ScreenshotImagePaths.Count > imageIndex ? hasScreenshots.ScreenshotImagePaths[imageIndex] : null;
             }
 
             if (imageType == ImageType.Chapter)
@@ -1611,7 +1584,9 @@ namespace MediaBrowser.Controller.Entities
                 return ItemRepository.GetChapter(Id, imageIndex).ImagePath;
             }
 
-            return GetImage(imageType);
+            string val;
+            Images.TryGetValue(imageType, out val);
+            return val;
         }
 
         /// <summary>
@@ -1658,5 +1633,21 @@ namespace MediaBrowser.Controller.Entities
         {
             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);
             }
 
-            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>();
         }
 

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

@@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="reason">The reason.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <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>
         /// Gets the user data.

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

@@ -37,6 +37,6 @@ namespace MediaBrowser.Controller.Library
         /// Gets or sets the item.
         /// </summary>
         /// <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; }
 
         /// <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>
-        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>
         /// <param name="id">The identifier.</param>
         /// <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>
         /// Gets the program.
         /// </summary>

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

@@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
         
         /// <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>
         /// <param name="channelId">The channel identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -87,7 +87,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task<ImageResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken);
 
         /// <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>
         /// <param name="recordingId">The recording identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task<ImageResponseInfo> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken);
 
         /// <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>
         /// <param name="programId">The program 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; }
 
         /// <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>
-        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>
         /// Gets or sets a value indicating whether this instance is movie.
@@ -120,7 +126,13 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
         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()
         {
             Genres = new List<string>();

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

@@ -114,10 +114,16 @@ namespace MediaBrowser.Controller.LiveTv
         public float? CommunityRating { get; set; }
 
         /// <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>
-        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()
         {

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

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

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

@@ -170,7 +170,7 @@ namespace MediaBrowser.Controller.MediaInfo
 
                         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
                         {
@@ -233,33 +233,23 @@ namespace MediaBrowser.Controller.MediaInfo
         /// <summary>
         /// Gets the subtitle cache path.
         /// </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="outputExtension">The output extension.</param>
         /// <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 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);
 

+ 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.IO;
 
@@ -13,43 +16,47 @@ namespace MediaBrowser.Controller.MediaInfo
         /// <summary>
         /// Gets the input argument.
         /// </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="playableStreamFileNames">The playable stream file names.</param>
         /// <param name="type">The type.</param>
         /// <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;
 
-            switch (video.VideoType)
+            switch (videoType)
             {
                 case VideoType.BluRay:
                     type = InputType.Bluray;
                     break;
                 case VideoType.Dvd:
                     type = InputType.Dvd;
-                    inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray();
+                    inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray();
                     break;
                 case VideoType.Iso:
-                    if (video.IsoType.HasValue)
+                    if (isoType.HasValue)
                     {
-                        switch (video.IsoType.Value)
+                        switch (isoType.Value)
                         {
                             case IsoType.BluRay:
                                 type = InputType.Bluray;
                                 break;
                             case IsoType.Dvd:
                                 type = InputType.Dvd;
-                                inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray();
+                                inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray();
                                 break;
                         }
                     }
                     break;
                 case VideoType.VideoFile:
                     {
-                        if (video.LocationType == LocationType.Remote)
+                        if (isRemote)
                         {
                             type = InputType.Url;
                         }
@@ -60,6 +67,17 @@ namespace MediaBrowser.Controller.MediaInfo
             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>
         /// Gets the type of the input.
         /// </summary>

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

@@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="item">The item.</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>
-        bool Supports(BaseItem item, ImageType imageType);
+        bool Supports(IHasImages item, ImageType imageType);
 
         /// <summary>
         /// 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="imageType">Type of the image.</param>
         /// <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>
         /// 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="originalImageSize">Size of the original image.</param>
         /// <returns>ImageSize.</returns>
-        ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize);
+        ImageSize GetEnhancedImageSize(IHasImages item, ImageType imageType, int imageIndex, ImageSize originalImageSize);
 
         /// <summary>
         /// Enhances the image async.
@@ -49,6 +49,6 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="imageIndex">Index of the image.</param>
         /// <returns>Task{Image}.</returns>
         /// <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;
 
             EnableMovieChapterImageExtraction = true;
-            EnableEpisodeChapterImageExtraction = true;
-            EnableOtherVideoChapterImageExtraction = true;
+            EnableEpisodeChapterImageExtraction = false;
+            EnableOtherVideoChapterImageExtraction = false;
 
 #if (DEBUG)
             EnableDeveloperTools = true;

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

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

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

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

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

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

+ 9 - 9
MediaBrowser.Providers/ImageFromMediaLocationProvider.cs

@@ -212,7 +212,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.Logo, image.FullName);
+                item.SetImagePath(ImageType.Logo, image.FullName);
             }
 
             // Clearart
@@ -220,7 +220,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.Art, image.FullName);
+                item.SetImagePath(ImageType.Art, image.FullName);
             }
 
             // Disc
@@ -229,7 +229,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.Disc, image.FullName);
+                item.SetImagePath(ImageType.Disc, image.FullName);
             }
 
             // Box Image
@@ -237,7 +237,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.Box, image.FullName);
+                item.SetImagePath(ImageType.Box, image.FullName);
             }
 
             // BoxRear Image
@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.BoxRear, image.FullName);
+                item.SetImagePath(ImageType.BoxRear, image.FullName);
             }
 
             // Thumbnail Image
@@ -253,7 +253,7 @@ namespace MediaBrowser.Providers
 
             if (image != null)
             {
-                item.SetImage(ImageType.Menu, image.FullName);
+                item.SetImagePath(ImageType.Menu, image.FullName);
             }
 
             PopulateBanner(item, args);
@@ -311,7 +311,7 @@ namespace MediaBrowser.Providers
 
             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)
             {
-                item.SetImage(ImageType.Banner, image.FullName);
+                item.SetImagePath(ImageType.Banner, image.FullName);
             }
         }
 
@@ -367,7 +367,7 @@ namespace MediaBrowser.Providers
 
             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>
         public override bool Supports(BaseItem item)
         {
-            return item is Channel;
+            return item is LiveTvChannel;
         }
 
         /// <summary>
@@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.LiveTv
 
                 try
                 {
-                    new BaseItemXmlParser<Channel>(Logger).Fetch((Channel)item, path, cancellationToken);
+                    new BaseItemXmlParser<LiveTvChannel>(Logger).Fetch((LiveTvChannel)item, path, cancellationToken);
                 }
                 finally
                 {

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

@@ -115,7 +115,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
             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);

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

@@ -253,7 +253,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 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);
 

+ 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 ((wasMetadataEdited || wasMetadataDownloaded))
             {
-                return item is Channel;
+                return item is LiveTvChannel;
             }
 
             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>
         /// <returns>Guid.</returns>
         /// <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)
             {
@@ -623,7 +623,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageEnhancers">The image enhancers.</param>
         /// <returns>Guid.</returns>
         /// <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)
             {
@@ -660,7 +660,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <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();
 
@@ -673,7 +673,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
             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,
                                                     List<IImageEnhancer> enhancers)
         {
@@ -709,7 +709,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="supportedEnhancers">The supported enhancers.</param>
         /// <returns>System.String.</returns>
         /// <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))
             {
@@ -782,7 +782,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <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;
 
@@ -900,7 +900,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
             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 =>
             {

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

@@ -818,7 +818,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                     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.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.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))
                 {
-                    dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImage(ImageType.Thumb));
+                    dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImagePath(ImageType.Thumb));
                 }
 
                 var imagePath = series.PrimaryImagePath;

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

@@ -49,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// userId
         /// or
         /// 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)
             {

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

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -21,18 +22,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private readonly ILiveTvManager _liveTvManager;
         private readonly IProviderManager _providerManager;
         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)
         {
             _liveTvManager = liveTvManager;
             _providerManager = providerManager;
             _fileSystem = fileSystem;
+            _httpClient = httpClient;
         }
 
         public override bool Supports(BaseItem item)
         {
-            return item is Channel;
+            return item is LiveTvChannel;
         }
 
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
@@ -48,21 +51,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 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;
         }
 
-        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
-                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;
         }
 
-        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
             {
                 Id = GetInternalRecordingId(service.Name, info.Id).ToString("N"),
+                Type = recording.GetClientTypeName(),
                 ChannelName = info.ChannelName,
                 Overview = info.Overview,
                 EndDate = info.EndDate,
@@ -154,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 EpisodeTitle = info.EpisodeTitle,
                 ChannelType = info.ChannelType,
                 MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video,
-                CommunityRating = info.CommunityRating,
+                CommunityRating = GetClientCommunityRating(info.CommunityRating),
                 OfficialRating = info.OfficialRating,
                 Audio = info.Audio,
                 IsHD = info.IsHD,
@@ -162,9 +180,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Url = info.Url
             };
 
+            var imageTag = GetImageTag(recording);
+
+            if (imageTag.HasValue)
+            {
+                dto.ImageTags[ImageType.Primary] = imageTag.Value;
+            }
+
             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;
@@ -184,18 +209,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         /// <param name="info">The info.</param>
         /// <param name="user">The user.</param>
         /// <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
             {
                 Name = info.Name,
                 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"),
                 MediaType = info.MediaType,
-                ExternalId = info.ChannelId
+                ExternalId = channelInfo.Id
             };
 
             if (user != null)
@@ -203,7 +230,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey()));
             }
 
-            var imageTag = GetLogoImageTag(info);
+            var imageTag = GetImageTag(info);
 
             if (imageTag.HasValue)
             {
@@ -213,7 +240,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             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
             {
@@ -230,7 +257,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 IsHD = program.IsHD,
                 OriginalAirDate = program.OriginalAirDate,
                 Audio = program.Audio,
-                CommunityRating = program.CommunityRating,
+                CommunityRating = GetClientCommunityRating(program.CommunityRating),
                 AspectRatio = program.AspectRatio,
                 IsRepeat = program.IsRepeat,
                 EpisodeTitle = program.EpisodeTitle,
@@ -248,7 +275,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return dto;
         }
 
-        private Guid? GetLogoImageTag(Channel info)
+        private Guid? GetImageTag(BaseItem info)
         {
             var path = info.PrimaryImagePath;
 
@@ -263,7 +290,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
             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;
@@ -273,7 +300,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
             var name = serviceName + externalId + channelName;
 
-            return name.ToLower().GetMBId(typeof(Channel));
+            return name.ToLower().GetMBId(typeof(LiveTvChannel));
         }
 
         public Guid GetInternalTimerId(string serviceName, string externalId)
@@ -314,41 +341,53 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Name = dto.Name,
                 StartDate = dto.StartDate,
                 Status = dto.Status,
-                SeriesTimerId = dto.ExternalSeriesTimerId,
                 PrePaddingSeconds = dto.PrePaddingSeconds,
                 PostPaddingSeconds = dto.PostPaddingSeconds,
                 IsPostPaddingRequired = dto.IsPostPaddingRequired,
                 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
-            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;
             }
 
-            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;
@@ -371,29 +410,38 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 Priority = dto.Priority,
                 RecordAnyChannel = dto.RecordAnyChannel,
                 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
-            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);
 
                 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);
 
-                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);
 
-                info.ProgramId = program.ExternalId;
+                if (program != null)
+                {
+                    info.ProgramId = program.ExternalId;
+                }
             }
 
             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 List<Channel> _channels = new List<Channel>();
+        private List<LiveTvChannel> _channels = new List<LiveTvChannel>();
         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)
@@ -77,18 +77,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
             var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
 
-            IEnumerable<Channel> channels = _channels;
+            IEnumerable<LiveTvChannel> channels = _channels;
 
             if (user != null)
             {
-                channels = channels.Where(i => i.IsParentalAllowed(user, _localization))
+                channels = channels
+                    .Where(i => i.IsParentalAllowed(user, _localization))
                     .OrderBy(i =>
                     {
                         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;
@@ -100,9 +101,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             {
                 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;
@@ -120,14 +121,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return Task.FromResult(result);
         }
 
-        public Channel GetChannel(string id)
+        public LiveTvChannel GetInternalChannel(string id)
         {
             var guid = new Guid(id);
 
             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));
 
@@ -150,26 +162,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
             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)
             {
-                item = new Channel
+                item = new LiveTvChannel
                 {
                     Name = channelInfo.Name,
                     Id = id,
                     DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo),
                     DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo),
-                    Path = path,
-                    ChannelId = channelInfo.Id,
-                    ChannelNumber = channelInfo.Number,
-                    ServiceName = serviceName,
-                    HasProviderImage = channelInfo.HasImage
+                    Path = path
                 };
 
                 isNew = true;
             }
 
+            item.ChannelInfo = channelInfo;
+            item.ServiceName = serviceName;
+
             // Set this now so we don't cause additional file system access during provider executions
             item.ResetResolveArgs(fileInfo);
 
@@ -178,6 +189,35 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             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)
         {
             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 allChannelsList = allChannels.ToList();
 
-            var list = new List<Channel>();
+            var list = new List<LiveTvChannel>();
             var programs = new List<ProgramInfoDto>();
 
             var numComplete = 0;
@@ -271,26 +311,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         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();
             }
 
-            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();
 
             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)
         {
             IEnumerable<ILiveTvService> services = _services;
@@ -404,11 +459,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
             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);
 
-            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)

+ 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\LiveTvDtoService.cs" />
     <Compile Include="LiveTv\LiveTvManager.cs" />
+    <Compile Include="LiveTv\ProgramImageProvider.cs" />
+    <Compile Include="LiveTv\RecordingImageProvider.cs" />
     <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
     <Compile Include="Localization\LocalizationManager.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;
             }
 
+            var index = imageIndex ?? 0;
+
             var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
 
             // 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;
             }
 
-            var currentPath = GetCurrentImagePath(item, type, imageIndex);
+            var currentPath = GetCurrentImagePath(item, type, index);
 
             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);
 
             // Delete the current path
@@ -257,27 +259,9 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// or
         /// imageIndex
         /// </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>
@@ -336,7 +320,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                     }
                     break;
                 default:
-                    item.SetImage(type, path);
+                    item.SetImagePath(type, path);
                     break;
             }
         }
@@ -593,7 +577,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="imageFilename">The image filename.</param>
         /// <param name="extension">The extension.</param>
         /// <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)
             {

+ 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
             foreach (var item in newItems
                 .Where(i => i.LocationType == LocationType.FileSystem && i.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(i.PrimaryImagePath) && i.DefaultVideoStreamIndex.HasValue)
-                .Take(2))
+                .Take(1))
             {
                 try
                 {

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

@@ -242,19 +242,19 @@ namespace MediaBrowser.ServerApplication
                                        }
                                        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))
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Logo), "Logo"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Logo), "Logo"));
                                        }
                                        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))
                                        {
-                                           previews.Add(new PreviewItem(item.GetImage(ImageType.Thumb), "Thumb"));
+                                           previews.Add(new PreviewItem(item.GetImagePath(ImageType.Thumb), "Thumb"));
                                        }
                                        previews.AddRange(
                                            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">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <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>
         <copyright>Copyright © Media Browser 2013</copyright>
         <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="SimpleInjector" version="2.4.0" />
             <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">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <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">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.278</version>
+        <version>3.0.281</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.278" />
+            <dependency id="MediaBrowser.Common" version="3.0.281" />
         </dependencies>
     </metadata>
     <files>