Bläddra i källkod

Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser

Eric Reed 11 år sedan
förälder
incheckning
6819be8160
100 ändrade filer med 3142 tillägg och 1015 borttagningar
  1. 2 16
      MediaBrowser.Api/BaseApiService.cs
  2. 2 15
      MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs
  3. 52 0
      MediaBrowser.Api/IHasItemFields.cs
  4. 102 2
      MediaBrowser.Api/Images/ImageService.cs
  5. 7 7
      MediaBrowser.Api/ItemRefreshService.cs
  6. 66 17
      MediaBrowser.Api/ItemUpdateService.cs
  7. 28 5
      MediaBrowser.Api/Library/LibraryStructureService.cs
  8. 33 35
      MediaBrowser.Api/LibraryService.cs
  9. 133 36
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  10. 13 9
      MediaBrowser.Api/MediaBrowser.Api.csproj
  11. 4 5
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  12. 16 12
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  13. 42 42
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  14. 12 8
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  15. 28 4
      MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
  16. 4 2
      MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs
  17. 2 4
      MediaBrowser.Api/SearchService.cs
  18. 1 27
      MediaBrowser.Api/SimilarItemsHelper.cs
  19. 261 24
      MediaBrowser.Api/TvShowsService.cs
  20. 3 18
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  21. 1 27
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  22. 160 34
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  23. 1 1
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  24. 9 1
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  25. 2 2
      MediaBrowser.Api/packages.config
  26. 13 9
      MediaBrowser.Common.Implementations/BaseApplicationHost.cs
  27. 6 4
      MediaBrowser.Common.Implementations/BaseApplicationPaths.cs
  28. 0 6
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientInfo.cs
  29. 274 248
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  30. 4 3
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  31. 0 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  32. 4 2
      MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs
  33. 1 1
      MediaBrowser.Common.Implementations/Updates/InstallationManager.cs
  34. 1 1
      MediaBrowser.Common.Implementations/packages.config
  35. 6 0
      MediaBrowser.Common/Configuration/IApplicationPaths.cs
  36. 12 9
      MediaBrowser.Common/MediaBrowser.Common.csproj
  37. 5 0
      MediaBrowser.Common/Net/IHttpClient.cs
  38. 5 0
      MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
  39. 11 1
      MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs
  40. 2 2
      MediaBrowser.Common/packages.config
  41. 0 86
      MediaBrowser.Controller/Entities/Audio/Artist.cs
  42. 79 3
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  43. 2 2
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  44. 55 34
      MediaBrowser.Controller/Entities/BaseItem.cs
  45. 1 1
      MediaBrowser.Controller/Entities/Extensions.cs
  46. 18 35
      MediaBrowser.Controller/Entities/Folder.cs
  47. 22 2
      MediaBrowser.Controller/Entities/Game.cs
  48. 2 2
      MediaBrowser.Controller/Entities/GameGenre.cs
  49. 4 4
      MediaBrowser.Controller/Entities/Genre.cs
  50. 14 0
      MediaBrowser.Controller/Entities/IHasAspectRatio.cs
  51. 18 0
      MediaBrowser.Controller/Entities/IHasBudget.cs
  52. 21 0
      MediaBrowser.Controller/Entities/IHasTrailers.cs
  53. 21 9
      MediaBrowser.Controller/Entities/IItemByName.cs
  54. 0 1
      MediaBrowser.Controller/Entities/IndexFolder.cs
  55. 18 2
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  56. 19 1
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  57. 13 1
      MediaBrowser.Controller/Entities/MusicVideo.cs
  58. 10 4
      MediaBrowser.Controller/Entities/Person.cs
  59. 2 2
      MediaBrowser.Controller/Entities/Studio.cs
  60. 69 15
      MediaBrowser.Controller/Entities/TV/Episode.cs
  61. 45 5
      MediaBrowser.Controller/Entities/TV/Season.cs
  62. 17 1
      MediaBrowser.Controller/Entities/TV/Series.cs
  63. 18 1
      MediaBrowser.Controller/Entities/Trailer.cs
  64. 19 1
      MediaBrowser.Controller/Entities/Video.cs
  65. 2 2
      MediaBrowser.Controller/Entities/Year.cs
  66. 14 1
      MediaBrowser.Controller/Library/ILibraryManager.cs
  67. 1 1
      MediaBrowser.Controller/Library/TVUtils.cs
  68. 73 0
      MediaBrowser.Controller/LiveTv/Channel.cs
  69. 0 6
      MediaBrowser.Controller/LiveTv/ChannelInfo.cs
  70. 84 6
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  71. 76 7
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  72. 19 0
      MediaBrowser.Controller/LiveTv/ImageResponseInfo.cs
  73. 98 0
      MediaBrowser.Controller/LiveTv/ProgramInfo.cs
  74. 102 0
      MediaBrowser.Controller/LiveTv/RecordingInfo.cs
  75. 85 0
      MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
  76. 73 0
      MediaBrowser.Controller/LiveTv/TimerInfo.cs
  77. 10 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  78. 97 13
      MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
  79. 39 0
      MediaBrowser.Controller/Providers/NameParser.cs
  80. 22 10
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  81. 22 10
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  82. 15 1
      MediaBrowser.Model/ApiClient/IApiClient.cs
  83. 6 0
      MediaBrowser.Model/Dto/BaseItemDto.cs
  84. 4 1
      MediaBrowser.Model/Dto/ItemByNameCounts.cs
  85. 0 23
      MediaBrowser.Model/LiveTv/ChannelGuide.cs
  86. 34 1
      MediaBrowser.Model/LiveTv/ChannelInfoDto.cs
  87. 27 0
      MediaBrowser.Model/LiveTv/ChannelQuery.cs
  88. 0 37
      MediaBrowser.Model/LiveTv/ProgramInfo.cs
  89. 120 0
      MediaBrowser.Model/LiveTv/ProgramInfoDto.cs
  90. 31 0
      MediaBrowser.Model/LiveTv/ProgramQuery.cs
  91. 119 0
      MediaBrowser.Model/LiveTv/RecordingInfoDto.cs
  92. 24 3
      MediaBrowser.Model/LiveTv/RecordingQuery.cs
  93. 22 0
      MediaBrowser.Model/LiveTv/RecordingStatus.cs
  94. 29 23
      MediaBrowser.Model/LiveTv/TimerInfoDto.cs
  95. 9 5
      MediaBrowser.Model/MediaBrowser.Model.csproj
  96. 45 0
      MediaBrowser.Model/Querying/EpisodeQuery.cs
  97. 4 0
      MediaBrowser.Model/Querying/ItemQuery.cs
  98. 1 0
      MediaBrowser.Model/Querying/ItemSortBy.cs
  99. 8 12
      MediaBrowser.Model/Querying/QueryResult.cs
  100. 12 0
      MediaBrowser.Model/System/SystemInfo.cs

+ 2 - 16
MediaBrowser.Api/BaseApiService.cs

@@ -92,7 +92,7 @@ namespace MediaBrowser.Api
         private readonly char[] _dashReplaceChars = new[] { '?', '/' };
         private const char SlugChar = '-';
 
-        protected Artist GetArtist(string name, ILibraryManager libraryManager)
+        protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
         {
             return libraryManager.GetArtist(DeSlugArtistName(name, libraryManager));
         }
@@ -147,21 +147,7 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren()
-                .OfType<Audio>()
-                .SelectMany(i =>
-                {
-                    var list = new List<string>();
-
-                    if (!string.IsNullOrEmpty(i.AlbumArtist))
-                    {
-                        list.Add(i.AlbumArtist);
-                    }
-                    list.AddRange(i.Artists);
-
-                    return list;
-                })
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+            return libraryManager.GetAllArtists()
                 .FirstOrDefault(i =>
                 {
                     i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));

+ 2 - 15
MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs

@@ -194,20 +194,7 @@ namespace MediaBrowser.Api.DefaultTheme
                 .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
                 .ToList();
 
-            var artists = allItems.OfType<Audio>()
-                .SelectMany(i =>
-            {
-                var list = new List<string>();
-
-                if (!string.IsNullOrEmpty(i.AlbumArtist))
-                {
-                    list.Add(i.AlbumArtist);
-                }
-                list.AddRange(i.Artists);
-
-                return list;
-            })
-            .Distinct(StringComparer.OrdinalIgnoreCase)
+            var artists = _libraryManager.GetAllArtists(allItems)
             .Randomize()
             .Select(i =>
             {
@@ -650,7 +637,7 @@ namespace MediaBrowser.Api.DefaultTheme
         public static IEnumerable<T> Randomize<T>(this IEnumerable<T> sequence, string type = "none")
             where T : BaseItem
         {
-            var hour = DateTime.Now.Hour + 2;
+            var hour = DateTime.Now.Hour + DateTime.Now.Day + 2;
 
             var typeCode = type.GetHashCode();
 

+ 52 - 0
MediaBrowser.Api/IHasItemFields.cs

@@ -0,0 +1,52 @@
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Interface IHasItemFields
+    /// </summary>
+    public interface IHasItemFields
+    {
+        /// <summary>
+        /// Gets or sets the fields.
+        /// </summary>
+        /// <value>The fields.</value>
+        string Fields { get; set; }
+    }
+
+    /// <summary>
+    /// Class ItemFieldsExtensions.
+    /// </summary>
+    public static class ItemFieldsExtensions
+    {
+        /// <summary>
+        /// Gets the item fields.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{ItemFields}.</returns>
+        public static IEnumerable<ItemFields> GetItemFields(this IHasItemFields request)
+        {
+            var val = request.Fields;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return new ItemFields[] { };
+            }
+
+            return val.Split(',').Select(v =>
+            {
+                ItemFields value;
+
+                if (Enum.TryParse(v, true, out value))
+                {
+                    return (ItemFields?)value;
+                }
+                return null;
+
+            }).Where(i => i.HasValue).Select(i => i.Value);
+        }
+    }
+}

+ 102 - 2
MediaBrowser.Api/Images/ImageService.cs

@@ -5,6 +5,7 @@ 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;
@@ -37,6 +38,18 @@ 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")]
@@ -67,6 +80,19 @@ namespace MediaBrowser.Api.Images
         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>
@@ -243,6 +269,19 @@ 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>
@@ -318,6 +357,25 @@ 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>
@@ -341,10 +399,12 @@ 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)
+        public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor, ILiveTvManager liveTv)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
@@ -353,6 +413,7 @@ namespace MediaBrowser.Api.Images
             _itemRepo = itemRepo;
             _dtoService = dtoService;
             _imageProcessor = imageProcessor;
+            _liveTv = liveTv;
         }
 
         /// <summary>
@@ -369,6 +430,15 @@ 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);
@@ -484,7 +554,7 @@ namespace MediaBrowser.Api.Images
                     Height = Convert.ToInt32(size.Height)
                 };
             }
-            catch (IOException ex)
+            catch (Exception ex)
             {
                 Logger.ErrorException("Error getting image information for {0}", ex, path);
 
@@ -492,6 +562,13 @@ 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>
@@ -577,6 +654,20 @@ namespace MediaBrowser.Api.Images
             Task.WaitAll(task);
         }
 
+        public void Post(PostChannelImage request)
+        {
+            var pathInfo = PathInfo.Parse(RequestContext.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, RequestContext.ContentType);
+
+            Task.WaitAll(task);
+        }
+        
         /// <summary>
         /// Deletes the specified request.
         /// </summary>
@@ -603,6 +694,15 @@ 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>

+ 7 - 7
MediaBrowser.Api/ItemRefreshService.cs

@@ -264,16 +264,14 @@ namespace MediaBrowser.Api
         {
             var item = _dtoService.GetItemByDtoId(request.Id);
 
-            var folder = item as Folder;
-
             try
             {
                 await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
 
-                if (folder != null)
+                if (item.IsFolder)
                 {
                     // Collection folders don't validate their children so we'll have to simulate that here
-                    var collectionFolder = folder as CollectionFolder;
+                    var collectionFolder = item as CollectionFolder;
 
                     if (collectionFolder != null)
                     {
@@ -281,6 +279,8 @@ namespace MediaBrowser.Api
                     }
                     else
                     {
+                        var folder = (Folder)item;
+
                         await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, request.Recursive, request.Forced).ConfigureAwait(false);
                     }
                 }
@@ -303,10 +303,10 @@ namespace MediaBrowser.Api
             {
                 await child.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
 
-                var folder = child as Folder;
-
-                if (folder != null)
+                if (child.IsFolder)
                 {
+                    var folder = (Folder)child;
+
                     await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, request.Recursive, request.Forced).ConfigureAwait(false);
                 }
             }

+ 66 - 17
MediaBrowser.Api/ItemUpdateService.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using ServiceStack.ServiceHost;
@@ -13,6 +14,14 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Api
 {
+    [Route("/LiveTv/Channels/{ChannelId}", "POST")]
+    [Api(("Updates an item"))]
+    public class UpdateChannel : BaseItemDto, IReturnVoid
+    {
+        [ApiMember(Name = "ChannelId", Description = "The id of the channel", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string ChannelId { get; set; }
+    }
+
     [Route("/Items/{ItemId}", "POST")]
     [Api(("Updates an item"))]
     public class UpdateItem : BaseItemDto, IReturnVoid
@@ -73,11 +82,13 @@ namespace MediaBrowser.Api
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
+        private readonly ILiveTvManager _liveTv;
 
-        public ItemUpdateService(ILibraryManager libraryManager, IDtoService dtoService)
+        public ItemUpdateService(ILibraryManager libraryManager, IDtoService dtoService, ILiveTvManager liveTv)
         {
             _libraryManager = libraryManager;
             _dtoService = dtoService;
+            _liveTv = liveTv;
         }
 
         public void Post(UpdateItem request)
@@ -87,13 +98,34 @@ namespace MediaBrowser.Api
             Task.WaitAll(task);
         }
 
-        private Task UpdateItem(UpdateItem request)
+        public void Post(UpdateChannel request)
+        {
+            var task = UpdateItem(request);
+
+            Task.WaitAll(task);
+        }
+
+        private async Task UpdateItem(UpdateItem request)
         {
             var item = _dtoService.GetItemByDtoId(request.ItemId);
 
+            var newEnableInternetProviders = request.EnableInternetProviders ?? true;
+            var dontFetchMetaChanged = item.DontFetchMeta != !newEnableInternetProviders;
+
             UpdateItem(request, item);
 
-            return _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None);
+            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+            if (dontFetchMetaChanged && item.IsFolder)
+            {
+                var folder = (Folder)item;
+
+                foreach (var child in folder.RecursiveChildren.ToList())
+                {
+                    child.DontFetchMeta = !newEnableInternetProviders;
+                    await _libraryManager.UpdateItem(child, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+                }
+            }
         }
 
         public void Post(UpdatePerson request)
@@ -112,6 +144,15 @@ namespace MediaBrowser.Api
             await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }
 
+        private async Task UpdateItem(UpdateChannel request)
+        {
+            var item = _liveTv.GetChannel(request.Id);
+
+            UpdateItem(request, item);
+
+            await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+        }
+
         public void Post(UpdateArtist request)
         {
             var task = UpdateItem(request);
@@ -126,15 +167,6 @@ namespace MediaBrowser.Api
             UpdateItem(request, item);
 
             await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
-            var musicArtist = Artist.FindMusicArtist(item, _libraryManager);
-
-            if (musicArtist != null)
-            {
-                UpdateItem(request, musicArtist);
-
-                await _libraryManager.UpdateItem(musicArtist, ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-            }
         }
 
         public void Post(UpdateStudio request)
@@ -216,8 +248,12 @@ namespace MediaBrowser.Api
                 item.ForcedSortName = request.SortName;
             }
 
-            item.Budget = request.Budget;
-            item.Revenue = request.Revenue;
+            var hasBudget = item as IHasBudget;
+            if (hasBudget != null)
+            {
+                hasBudget.Budget = request.Budget;
+                hasBudget.Revenue = request.Revenue;
+            }
 
             var hasCriticRating = item as IHasCriticRating;
             if (hasCriticRating != null)
@@ -235,8 +271,16 @@ namespace MediaBrowser.Api
             item.Overview = request.Overview;
             item.Genres = request.Genres;
             item.Tags = request.Tags;
-            item.Studios = request.Studios.Select(x => x.Name).ToList();
-            item.People = request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList();
+
+            if (request.Studios != null)
+            {
+                item.Studios = request.Studios.Select(x => x.Name).ToList();
+            }
+
+            if (request.People != null)
+            {
+                item.People = request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList();
+            }
 
             if (request.DateCreated.HasValue)
             {
@@ -247,11 +291,16 @@ namespace MediaBrowser.Api
             item.PremiereDate = request.PremiereDate.HasValue ? request.PremiereDate.Value.ToUniversalTime() : (DateTime?)null;
             item.ProductionYear = request.ProductionYear;
             item.ProductionLocations = request.ProductionLocations;
-            item.AspectRatio = request.AspectRatio;
             item.Language = request.Language;
             item.OfficialRating = request.OfficialRating;
             item.CustomRating = request.CustomRating;
 
+            var hasAspectRatio = item as IHasAspectRatio;
+            if (hasAspectRatio != null)
+            {
+                hasAspectRatio.AspectRatio = request.AspectRatio;
+            }
+            
             item.DontFetchMeta = !(request.EnableInternetProviders ?? true);
             if (request.EnableInternetProviders ?? true)
             {

+ 28 - 5
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -286,7 +286,12 @@ namespace MediaBrowser.Api.Library
             }
             finally
             {
-                _directoryWatchers.Start();
+                // No need to start if scanning the library because it will handle it
+                if (!request.RefreshLibrary)
+                {
+                    _directoryWatchers.Start();
+                }
+
                 _directoryWatchers.RemoveTempIgnore(virtualFolderPath);
             }
 
@@ -353,7 +358,12 @@ namespace MediaBrowser.Api.Library
             }
             finally
             {
-                _directoryWatchers.Start();
+                // No need to start if scanning the library because it will handle it
+                if (!request.RefreshLibrary)
+                {
+                    _directoryWatchers.Start();
+                }
+
                 _directoryWatchers.RemoveTempIgnore(currentPath);
                 _directoryWatchers.RemoveTempIgnore(newPath);
             }
@@ -404,7 +414,12 @@ namespace MediaBrowser.Api.Library
             }
             finally
             {
-                _directoryWatchers.Start();
+                // No need to start if scanning the library because it will handle it
+                if (!request.RefreshLibrary)
+                {
+                    _directoryWatchers.Start();
+                }
+
                 _directoryWatchers.RemoveTempIgnore(path);
             }
 
@@ -442,7 +457,11 @@ namespace MediaBrowser.Api.Library
             }
             finally
             {
-                _directoryWatchers.Start();
+                // No need to start if scanning the library because it will handle it
+                if (!request.RefreshLibrary)
+                {
+                    _directoryWatchers.Start();
+                }
             }
 
             if (request.RefreshLibrary)
@@ -479,7 +498,11 @@ namespace MediaBrowser.Api.Library
             }
             finally
             {
-                _directoryWatchers.Start();
+                // No need to start if scanning the library because it will handle it
+                if (!request.RefreshLibrary)
+                {
+                    _directoryWatchers.Start();
+                }
             }
 
             if (request.RefreshLibrary)

+ 33 - 35
MediaBrowser.Api/LibraryService.cs

@@ -37,7 +37,7 @@ namespace MediaBrowser.Api
     /// </summary>
     [Route("/Items/{Id}/CriticReviews", "GET")]
     [Api(Description = "Gets critic reviews for an item")]
-    public class GetCriticReviews : IReturn<ItemReviewsResult>
+    public class GetCriticReviews : IReturn<QueryResult<ItemReview>>
     {
         /// <summary>
         /// Gets or sets the id.
@@ -367,7 +367,7 @@ namespace MediaBrowser.Api
                 BoxSetCount = boxsets.Count,
                 BookCount = books.Count,
 
-                UniqueTypes = items.Select(i => i.GetType().Name).Distinct().ToList()
+                UniqueTypes = items.Select(i => i.GetClientTypeName()).Distinct().ToList()
             };
 
             var people = items.SelectMany(i => i.People)
@@ -390,19 +390,7 @@ namespace MediaBrowser.Api
             people = request.UserId.HasValue ? FilterItems(people, request, request.UserId.Value).ToList() : people;
             counts.PersonCount = people.Count;
 
-            var artists = items.OfType<Audio>().SelectMany(i =>
-            {
-                var list = new List<string>();
-
-                if (!string.IsNullOrEmpty(i.AlbumArtist))
-                {
-                    list.Add(i.AlbumArtist);
-                }
-                list.AddRange(i.Artists);
-
-                return list;
-            })
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+            var artists = _libraryManager.GetAllArtists(items)
                 .Select(i =>
                 {
                     try
@@ -477,13 +465,16 @@ namespace MediaBrowser.Api
 
             if (item.LocationType == LocationType.FileSystem)
             {
-                if (Directory.Exists(item.Path))
-                {
-                    Directory.Delete(item.Path, true);
-                }
-                else if (File.Exists(item.Path))
+                foreach (var path in item.GetDeletePaths().ToList())
                 {
-                    File.Delete(item.Path);
+                    if (Directory.Exists(path))
+                    {
+                        Directory.Delete(path, true);
+                    }
+                    else if (File.Exists(path))
+                    {
+                        File.Delete(path);
+                    }
                 }
 
                 if (parent != null)
@@ -521,16 +512,16 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>Task{ItemReviewsResult}.</returns>
-        private ItemReviewsResult GetCriticReviews(GetCriticReviews request)
+        private QueryResult<ItemReview> GetCriticReviews(GetCriticReviews request)
         {
             var reviews = _itemRepo.GetCriticReviews(new Guid(request.Id));
 
             var reviewsArray = reviews.ToArray();
 
-            var result = new ItemReviewsResult
-                {
-                    TotalRecordCount = reviewsArray.Length
-                };
+            var result = new QueryResult<ItemReview>
+            {
+                TotalRecordCount = reviewsArray.Length
+            };
 
             if (request.StartIndex.HasValue)
             {
@@ -541,7 +532,7 @@ namespace MediaBrowser.Api
                 reviewsArray = reviewsArray.Take(request.Limit.Value).ToArray();
             }
 
-            result.ItemReviews = reviewsArray;
+            result.Items = reviewsArray;
 
             return result;
         }
@@ -681,6 +672,11 @@ namespace MediaBrowser.Api
             {
                 var album = originalItem as MusicAlbum;
 
+                if (album == null)
+                {
+                    album = originalItem.Parents.OfType<MusicAlbum>().FirstOrDefault();
+                }
+
                 if (album != null)
                 {
                     var linkedItemWithThemes = album.SoundtrackIds
@@ -744,17 +740,12 @@ namespace MediaBrowser.Api
                                   : (Folder)_libraryManager.RootFolder)
                            : _dtoService.GetItemByDtoId(id, userId);
 
-            while (GetSoundtrackSongIds(item).Count == 0 && inheritFromParent && item.Parent != null)
-            {
-                item = item.Parent;
-            }
-
             // Get everything
             var fields = Enum.GetNames(typeof(ItemFields))
                     .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
                     .ToList();
 
-            var dtos = GetSoundtrackSongIds(item)
+            var dtos = GetSoundtrackSongIds(item, inheritFromParent)
                 .Select(_libraryManager.GetItemById)
                 .OfType<MusicAlbum>()
                 .SelectMany(i => i.RecursiveChildren)
@@ -772,7 +763,7 @@ namespace MediaBrowser.Api
             };
         }
 
-        private List<Guid> GetSoundtrackSongIds(BaseItem item)
+        private IEnumerable<Guid> GetSoundtrackSongIds(BaseItem item, bool inherit)
         {
             var hasSoundtracks = item as IHasSoundtracks;
 
@@ -781,7 +772,14 @@ namespace MediaBrowser.Api
                 return hasSoundtracks.SoundtrackIds;
             }
 
-            return new List<Guid>();
+            if (!inherit)
+            {
+                return null;
+            }
+
+            hasSoundtracks = item.Parents.OfType<IHasSoundtracks>().FirstOrDefault();
+
+            return hasSoundtracks != null ? hasSoundtracks.SoundtrackIds : new List<Guid>();
         }
     }
 }

+ 133 - 36
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -1,6 +1,8 @@
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
@@ -18,31 +20,101 @@ namespace MediaBrowser.Api.LiveTv
 
     [Route("/LiveTv/Channels", "GET")]
     [Api(Description = "Gets available live tv channels.")]
-    public class GetChannels : IReturn<List<ChannelInfoDto>>
+    public class GetChannels : IReturn<QueryResult<ChannelInfoDto>>
     {
         [ApiMember(Name = "ServiceName", Description = "Optional filter by service.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string ServiceName { get; set; }
+
+        [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public ChannelType? Type { get; set; }
+
+        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { get; set; }
+    }
+
+    [Route("/LiveTv/Channels/{Id}", "GET")]
+    [Api(Description = "Gets a live tv channel")]
+    public class GetChannel : IReturn<ChannelInfoDto>
+    {
+        /// <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; }
+
+        [ApiMember(Name = "UserId", Description = "Optional user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { get; set; }
     }
 
     [Route("/LiveTv/Recordings", "GET")]
-    [Api(Description = "Gets available live tv recordings.")]
-    public class GetRecordings : IReturn<List<RecordingInfo>>
+    [Api(Description = "Gets live tv recordings")]
+    public class GetRecordings : IReturn<QueryResult<RecordingInfoDto>>
     {
         [ApiMember(Name = "ServiceName", Description = "Optional filter by service.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string ServiceName { get; set; }
+
+        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ChannelId { get; set; }
     }
 
-    [Route("/LiveTv/Guide", "GET")]
+    [Route("/LiveTv/Recordings/{Id}", "GET")]
+    [Api(Description = "Gets a live tv recording")]
+    public class GetRecording : IReturn<RecordingInfoDto>
+    {
+        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Timers/{Id}", "GET")]
+    [Api(Description = "Gets a live tv timer")]
+    public class GetTimer : IReturn<TimerInfoDto>
+    {
+        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Timers", "GET")]
+    [Api(Description = "Gets live tv timers")]
+    public class GetTimers : IReturn<QueryResult<TimerInfoDto>>
+    {
+        [ApiMember(Name = "ServiceName", Description = "Optional filter by service.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ServiceName { get; set; }
+
+        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ChannelId { get; set; }
+    }
+
+    [Route("/LiveTv/Programs", "GET")]
     [Api(Description = "Gets available live tv epgs..")]
-    public class GetGuide : IReturn<List<ChannelGuide>>
+    public class GetPrograms : IReturn<QueryResult<ProgramInfoDto>>
     {
-        [ApiMember(Name = "ServiceName", Description = "Live tv service name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        [ApiMember(Name = "ServiceName", Description = "Live tv service name", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string ServiceName { get; set; }
 
-        [ApiMember(Name = "ChannelIds", Description = "The channels to return guide information for.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        [ApiMember(Name = "ChannelIds", Description = "The channels to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string ChannelIds { get; set; }
+
+        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { get; set; }
     }
 
+    [Route("/LiveTv/Recordings/{Id}", "DELETE")]
+    [Api(Description = "Deletes a live tv recording")]
+    public class DeleteRecording : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/Timers/{Id}", "DELETE")]
+    [Api(Description = "Cancels a live tv timer")]
+    public class CancelTimer : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+    
     public class LiveTvService : BaseApiService
     {
         private readonly ILiveTvManager _liveTvManager;
@@ -58,7 +130,7 @@ namespace MediaBrowser.Api.LiveTv
 
             if (!string.IsNullOrEmpty(serviceName))
             {
-                services = services.Where(i => string.Equals(i.Name, serviceName, System.StringComparison.OrdinalIgnoreCase));
+                services = services.Where(i => string.Equals(i.Name, serviceName, StringComparison.OrdinalIgnoreCase));
             }
 
             return services;
@@ -83,62 +155,87 @@ namespace MediaBrowser.Api.LiveTv
 
         public object Get(GetChannels request)
         {
-            var result = GetChannelsAsync(request).Result;
+            var result = _liveTvManager.GetChannels(new ChannelQuery
+            {
+                ChannelType = request.Type,
+                ServiceName = request.ServiceName,
+                UserId = request.UserId
 
-            return ToOptimizedResult(result.ToList());
+            });
+
+            return ToOptimizedResult(result);
         }
 
-        private async Task<IEnumerable<ChannelInfoDto>> GetChannelsAsync(GetChannels request)
+        public object Get(GetChannel request)
         {
-            var services = GetServices(request.ServiceName);
+            var result = _liveTvManager.GetChannelInfoDto(request.Id, request.UserId);
+
+            return ToOptimizedResult(result);
+        }
 
-            var tasks = services.Select(i => i.GetChannelsAsync(CancellationToken.None));
+        public object Get(GetPrograms request)
+        {
+            var result = _liveTvManager.GetPrograms(new ProgramQuery
+            {
+                ServiceName = request.ServiceName,
+                ChannelIdList = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(),
+                UserId = request.UserId
 
-            var channelLists = await Task.WhenAll(tasks).ConfigureAwait(false);
+            }, CancellationToken.None).Result;
 
-            // Aggregate all channels from all services
-            return channelLists.SelectMany(i => i)
-                .Select(_liveTvManager.GetChannelInfoDto);
+            return ToOptimizedResult(result);
         }
 
         public object Get(GetRecordings request)
         {
-            var result = GetRecordingsAsync(request).Result;
+            var result = _liveTvManager.GetRecordings(new RecordingQuery
+            {
+                ChannelId = request.ChannelId,
+                ServiceName = request.ServiceName
 
-            return ToOptimizedResult(result.ToList());
+            }, CancellationToken.None).Result;
+
+            return ToOptimizedResult(result);
         }
 
-        private async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(GetRecordings request)
+        public object Get(GetRecording request)
         {
-            var services = GetServices(request.ServiceName);
-
-            var query = new RecordingQuery
-            {
-
-            };
+            var result = _liveTvManager.GetRecording(request.Id, CancellationToken.None).Result;
 
-            var tasks = services.Select(i => i.GetRecordingsAsync(query, CancellationToken.None));
+            return ToOptimizedResult(result);
+        }
 
-            var recordings = await Task.WhenAll(tasks).ConfigureAwait(false);
+        public object Get(GetTimer request)
+        {
+            var result = _liveTvManager.GetTimer(request.Id, CancellationToken.None).Result;
 
-            return recordings.SelectMany(i => i);
+            return ToOptimizedResult(result);
         }
 
-        public object Get(GetGuide request)
+        public object Get(GetTimers request)
         {
-            var result = GetGuideAsync(request).Result;
+            var result = _liveTvManager.GetTimers(new TimerQuery
+            {
+                ChannelId = request.ChannelId,
+                ServiceName = request.ServiceName
+
+            }, CancellationToken.None).Result;
 
             return ToOptimizedResult(result);
         }
 
-        private async Task<IEnumerable<ChannelGuide>> GetGuideAsync(GetGuide request)
+        public void Delete(DeleteRecording request)
         {
-            var service = GetServices(request.ServiceName)
-                .First();
+            var task = _liveTvManager.DeleteRecording(request.Id);
 
-            var channels = request.ChannelIds.Split(',');
+            Task.WaitAll(task);
+        }
+
+        public void Delete(CancelTimer request)
+        {
+            var task = _liveTvManager.CancelTimer(request.Id);
 
-            return await service.GetChannelGuidesAsync(channels, CancellationToken.None).ConfigureAwait(false);
+            Task.WaitAll(task);
         }
     }
 }

+ 13 - 9
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -38,6 +38,18 @@
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="ServiceStack.Common, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Common.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Interfaces, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Text, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.70\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="Microsoft.CSharp" />
@@ -47,15 +59,6 @@
     <Reference Include="MoreLinq">
       <HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath>
     </Reference>
-    <Reference Include="ServiceStack.Common">
-      <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll</HintPath>
-    </Reference>
-    <Reference Include="ServiceStack.Interfaces">
-      <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll</HintPath>
-    </Reference>
-    <Reference Include="ServiceStack.Text">
-      <HintPath>..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll</HintPath>
-    </Reference>
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
@@ -70,6 +73,7 @@
     <Compile Include="EnvironmentService.cs" />
     <Compile Include="AuthorizationRequestFilterAttribute.cs" />
     <Compile Include="GamesService.cs" />
+    <Compile Include="IHasItemFields.cs" />
     <Compile Include="Images\ImageByNameService.cs" />
     <Compile Include="Images\ImageRequest.cs" />
     <Compile Include="Images\ImageService.cs" />

+ 4 - 5
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,5 +1,4 @@
-using System.Globalization;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller;
@@ -11,15 +10,15 @@ using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -696,7 +695,7 @@ namespace MediaBrowser.Api.Playback
             // This is arbitrary, but add a little buffer time when internet streaming
             if (state.Item.LocationType == LocationType.Remote)
             {
-                await Task.Delay(2000).ConfigureAwait(false);
+                await Task.Delay(4000).ConfigureAwait(false);
             }
         }
 

+ 16 - 12
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -88,7 +88,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 }
 
                 var volParam = string.Empty;
-                var AudioSampleRate = string.Empty;
+                var audioSampleRate = string.Empty;
 
                 // Boost volume to 200% when downsampling from 6ch to 2ch
                 if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
@@ -98,10 +98,10 @@ namespace MediaBrowser.Api.Playback.Hls
                 
                 if (state.Request.AudioSampleRate.HasValue)
                 {
-                    AudioSampleRate= state.Request.AudioSampleRate.Value + ":";
+                    audioSampleRate= state.Request.AudioSampleRate.Value + ":";
                 }
 
-                args += string.Format(" -af \"adelay=1,aresample={0}async=1000{1}\"",AudioSampleRate, volParam);
+                args += string.Format(" -af \"adelay=1,aresample={0}async=1000{1}\"",audioSampleRate, volParam);
 
                 return args;
             }
@@ -127,6 +127,10 @@ namespace MediaBrowser.Api.Playback.Hls
 
             const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
 
+            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsExternal &&
+                                 (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 ||
+                                  state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1);
+            
             var args = "-codec:v:0 " + codec + " -preset superfast" + keyFrameArg;
 
             var bitrate = GetVideoBitrateParam(state);
@@ -137,9 +141,12 @@ namespace MediaBrowser.Api.Playback.Hls
             }
             
             // Add resolution params, if specified
-            if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue)
+            if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec, performSubtitleConversion);
+                if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue)
+                {
+                    args += GetOutputSizeParam(state, codec, performSubtitleConversion);
+                }
             }
 
             if (state.VideoRequest.Framerate.HasValue)
@@ -158,14 +165,11 @@ namespace MediaBrowser.Api.Playback.Hls
             {
                 args += " -level " + state.VideoRequest.Level;
             }
-            
-            if (state.SubtitleStream != null)
+
+            // This is for internal graphical subs
+            if (hasGraphicalSubs)
             {
-                // This is for internal graphical subs
-                if (!state.SubtitleStream.IsExternal && (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1))
-                {
-                    args += GetInternalGraphicalSubtitleParam(state, codec);
-                }
+                args += GetInternalGraphicalSubtitleParam(state, codec);
             }
          
             return args;

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

@@ -127,44 +127,44 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             const string dlnaflags = ";DLNA.ORG_FLAGS=01500000000000000000000000000000";
 
-            if (string.Equals(extension, ".mp3", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=MP3";
-            }
-            else if (string.Equals(extension, ".aac", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=AAC_ISO";
-            }
-            else if (string.Equals(extension, ".wma", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=WMABASE";
-            }
-            else if (string.Equals(extension, ".avi", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=AVI";
-            }
-            else if (string.Equals(extension, ".mp4", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=MPEG4_P2_SP_AAC";
-            }
-            else if (string.Equals(extension, ".mpeg", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL";
-            }
-            else if (string.Equals(extension, ".wmv", StringComparison.OrdinalIgnoreCase))
-            {
-                contentFeatures = "DLNA.ORG_PN=WMVHIGH_BASE";
-            }
-            else if (string.Equals(extension, ".asf", StringComparison.OrdinalIgnoreCase))
-            {
-                // ??
-                contentFeatures = "DLNA.ORG_PN=WMVHIGH_BASE";
-            }
-            else if (string.Equals(extension, ".mkv", StringComparison.OrdinalIgnoreCase))
-            {
-                // ??
-                contentFeatures = "";
-            }
+            //if (string.Equals(extension, ".mp3", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=MP3";
+            //}
+            //else if (string.Equals(extension, ".aac", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=AAC_ISO";
+            //}
+            //else if (string.Equals(extension, ".wma", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=WMABASE";
+            //}
+            //else if (string.Equals(extension, ".avi", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=AVI";
+            //}
+            //else if (string.Equals(extension, ".mp4", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=MPEG4_P2_SP_AAC";
+            //}
+            //else if (string.Equals(extension, ".mpeg", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL";
+            //}
+            //else if (string.Equals(extension, ".wmv", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    contentFeatures = "DLNA.ORG_PN=WMVHIGH_BASE";
+            //}
+            //else if (string.Equals(extension, ".asf", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    // ??
+            //    contentFeatures = "DLNA.ORG_PN=WMVHIGH_BASE";
+            //}
+            //else if (string.Equals(extension, ".mkv", StringComparison.OrdinalIgnoreCase))
+            //{
+            //    // ??
+            //    contentFeatures = "";
+            //}
 
             if (!string.IsNullOrEmpty(contentFeatures))
             {
@@ -206,10 +206,10 @@ namespace MediaBrowser.Api.Playback.Progressive
             var outputPath = GetOutputFilePath(state);
             var outputPathExists = File.Exists(outputPath);
 
-            //var isStatic = request.Static ||
-            //               (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive));
+            var isStatic = request.Static ||
+                           (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive));
 
-            //AddDlnaHeaders(state, responseHeaders, isStatic);
+            AddDlnaHeaders(state, responseHeaders, isStatic);
 
             if (request.Static)
             {
@@ -307,7 +307,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 }
             }
 
-            return new ImageService(UserManager, LibraryManager, ApplicationPaths, null, ItemRepository, DtoService, ImageProcessor)
+            return new ImageService(UserManager, LibraryManager, ApplicationPaths, null, ItemRepository, DtoService, ImageProcessor, null)
             {
                 Logger = Logger,
                 RequestContext = RequestContext,

+ 12 - 8
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -143,12 +143,19 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             args += keyFrameArg;
 
+            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsExternal &&
+                                   (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 ||
+                                    state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1);
+
             var request = state.VideoRequest;
 
             // Add resolution params, if specified
-            if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+            if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec, performSubtitleConversion);
+                if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+                {
+                    args += GetOutputSizeParam(state, codec, performSubtitleConversion);
+                }
             }
 
             if (request.Framerate.HasValue)
@@ -175,13 +182,10 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -level " + state.VideoRequest.Level;
             }
 
-            if (state.SubtitleStream != null)
+            // This is for internal graphical subs
+            if (hasGraphicalSubs)
             {
-                // This is for internal graphical subs
-                if (!state.SubtitleStream.IsExternal && (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1))
-                {
-                    args += GetInternalGraphicalSubtitleParam(state, codec);
-                }
+                args += GetInternalGraphicalSubtitleParam(state, codec);
             }
 
             return args;

+ 28 - 4
MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs

@@ -31,7 +31,8 @@ namespace MediaBrowser.Api.ScheduledTasks
     [Api(Description = "Gets scheduled tasks")]
     public class GetScheduledTasks : IReturn<List<TaskInfo>>
     {
-
+        [ApiMember(Name = "IsHidden", Description = "Optional filter tasks that are hidden, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsHidden { get; set; }
     }
 
     /// <summary>
@@ -112,10 +113,33 @@ namespace MediaBrowser.Api.ScheduledTasks
         /// <returns>IEnumerable{TaskInfo}.</returns>
         public object Get(GetScheduledTasks request)
         {
-            var result = TaskManager.ScheduledTasks.OrderBy(i => i.Name)
-                         .Select(ScheduledTaskHelpers.GetTaskInfo).ToList();
+            IEnumerable<IScheduledTaskWorker> result = TaskManager.ScheduledTasks
+                .OrderBy(i => i.Name);
 
-            return ToOptimizedResult(result);
+            if (request.IsHidden.HasValue)
+            {
+                var val = request.IsHidden.Value;
+
+                result = result.Where(i =>
+                {
+                    var isHidden = false;
+
+                    var configurableTask = i.ScheduledTask as IConfigurableScheduledTask;
+
+                    if (configurableTask != null)
+                    {
+                        isHidden = configurableTask.IsHidden;
+                    }
+
+                    return isHidden == val;
+                });
+            }
+
+            var infos = result
+                .Select(ScheduledTaskHelpers.GetTaskInfo)
+                .ToList();
+
+            return ToOptimizedResult(infos);
         }
 
         /// <summary>

+ 4 - 2
MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs

@@ -46,8 +46,10 @@ namespace MediaBrowser.Api.ScheduledTasks
         /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
         protected override Task<IEnumerable<TaskInfo>> GetDataToSend(object state)
         {
-            return Task.FromResult(TaskManager.ScheduledTasks.OrderBy(i => i.Name)
-                         .Select(ScheduledTaskHelpers.GetTaskInfo));
+            return Task.FromResult(TaskManager.ScheduledTasks
+                .OrderBy(i => i.Name)
+                .Select(ScheduledTaskHelpers.GetTaskInfo)
+                .Where(i => !i.IsHidden));
         }
     }
 }

+ 2 - 4
MediaBrowser.Api/SearchService.cs

@@ -144,7 +144,7 @@ namespace MediaBrowser.Api
                 IndexNumber = item.IndexNumber,
                 ParentIndexNumber = item.ParentIndexNumber,
                 ItemId = _dtoService.GetDtoId(item),
-                Type = item.GetType().Name,
+                Type = item.GetClientTypeName(),
                 MediaType = item.MediaType,
                 MatchedTerm = hintInfo.MatchedTerm,
                 DisplayMediaType = item.DisplayMediaType,
@@ -187,9 +187,7 @@ namespace MediaBrowser.Api
 
                 result.SongCount = songs.Count;
                 
-                result.Artists = songs
-                    .SelectMany(i => i.Artists)
-                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                result.Artists = _libraryManager.GetAllArtists(songs)
                     .ToArray();
 
                 result.AlbumArtist = songs.Select(i => i.AlbumArtist).FirstOrDefault(i => !string.IsNullOrEmpty(i));

+ 1 - 27
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Api
         public string Id { get; set; }
     }
 
-    public class BaseGetSimilarItems : IReturn<ItemsResult>
+    public class BaseGetSimilarItems : IReturn<ItemsResult>, IHasItemFields
     {
         /// <summary>
         /// Gets or sets the user id.
@@ -47,32 +47,6 @@ namespace MediaBrowser.Api
         /// <value>The fields.</value>
         [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, OverviewHtml, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string Fields { get; set; }
-
-        /// <summary>
-        /// Gets the item fields.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        public IEnumerable<ItemFields> GetItemFields()
-        {
-            var val = Fields;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return new ItemFields[] { };
-            }
-
-            return val.Split(',').Select(v =>
-            {
-                ItemFields value;
-
-                if (Enum.TryParse(v, true, out value))
-                {
-                    return (ItemFields?)value;
-                }
-                return null;
-
-            }).Where(i => i.HasValue).Select(i => i.Value);
-        }
     }
 
     /// <summary>

+ 261 - 24
MediaBrowser.Api/TvShowsService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Dto;
+using MediaBrowser.Api.UserLibrary;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -17,7 +18,7 @@ namespace MediaBrowser.Api
     /// </summary>
     [Route("/Shows/NextUp", "GET")]
     [Api(("Gets a list of currently installed plugins"))]
-    public class GetNextUpEpisodes : IReturn<ItemsResult>
+    public class GetNextUpEpisodes : IReturn<ItemsResult>, IHasItemFields
     {
         /// <summary>
         /// Gets or sets the user id.
@@ -49,38 +50,83 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "SeriesId", Description = "Optional. Filter by series id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string SeriesId { get; set; }
+    }
+
+    [Route("/Shows/{Id}/Similar", "GET")]
+    [Api(Description = "Finds tv shows similar to a given one.")]
+    public class GetSimilarShows : BaseGetSimilarItemsFromItem
+    {
+    }
 
+    [Route("/Shows/{Id}/Episodes", "GET")]
+    [Api(Description = "Gets episodes for a tv season")]
+    public class GetEpisodes : IReturn<ItemsResult>, IHasItemFields
+    {
         /// <summary>
-        /// Gets the item fields.
+        /// Gets or sets the user id.
         /// </summary>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        public IEnumerable<ItemFields> GetItemFields()
-        {
-            var val = Fields;
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid UserId { get; set; }
 
-            if (string.IsNullOrEmpty(val))
-            {
-                return new ItemFields[] { };
-            }
+        /// <summary>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, OverviewHtml, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Fields { get; set; }
 
-            return val.Split(',').Select(v =>
-            {
-                ItemFields value;
+        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid Id { get; set; }
 
-                if (Enum.TryParse(v, true, out value))
-                {
-                    return (ItemFields?)value;
-                }
-                return null;
+        [ApiMember(Name = "Season", Description = "Optional filter by season number.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public int? Season { get; set; }
 
-            }).Where(i => i.HasValue).Select(i => i.Value);
-        }
+        [ApiMember(Name = "SeasonId", Description = "Optional. Filter by season id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string SeasonId { get; set; }
+        
+        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsMissing { get; set; }
+
+        [ApiMember(Name = "IsVirtualUnaired", Description = "Optional filter by items that are virtual unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsVirtualUnaired { get; set; }
+
+        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string AdjacentTo { get; set; }
     }
 
-    [Route("/Shows/{Id}/Similar", "GET")]
-    [Api(Description = "Finds tv shows similar to a given one.")]
-    public class GetSimilarShows : BaseGetSimilarItemsFromItem
+    [Route("/Shows/{Id}/Seasons", "GET")]
+    [Api(Description = "Gets seasons for a tv series")]
+    public class GetSeasons : IReturn<ItemsResult>, IHasItemFields
     {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, OverviewHtml, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Fields { get; set; }
+
+        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid Id { get; set; }
+
+        [ApiMember(Name = "IsSpecialSeason", Description = "Optional. Filter by special season.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsSpecialSeason { get; set; }
+
+        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsMissing { get; set; }
+
+        [ApiMember(Name = "IsVirtualUnaired", Description = "Optional filter by items that are virtual unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsVirtualUnaired { get; set; }
+
+        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string AdjacentTo { get; set; }
     }
 
     /// <summary>
@@ -311,5 +357,196 @@ namespace MediaBrowser.Api
 
             return items;
         }
+
+        public object Get(GetSeasons request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var series = _libraryManager.GetItemById(request.Id) as Series;
+
+            var fields = request.GetItemFields().ToList();
+
+            var seasons = series.GetChildren(user, true)
+                .OfType<Season>();
+
+            var sortOrder = ItemSortBy.SortName;
+
+            if (request.IsSpecialSeason.HasValue)
+            {
+                var val = request.IsSpecialSeason.Value;
+
+                seasons = seasons.Where(i => i.IsSpecialSeason == val);
+            }
+
+            var config = user.Configuration;
+
+            if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes)
+            {
+                seasons = seasons.Where(i => !i.IsMissingOrVirtualUnaired);
+            }
+            else
+            {
+                if (!config.DisplayMissingEpisodes)
+                {
+                    seasons = seasons.Where(i => !i.IsMissingSeason);
+                }
+                if (!config.DisplayUnairedEpisodes)
+                {
+                    seasons = seasons.Where(i => !i.IsVirtualUnaired);
+                }
+            }
+
+            seasons = FilterVirtualSeasons(request, seasons);
+
+            seasons = _libraryManager.Sort(seasons, user, new[] { sortOrder }, SortOrder.Ascending)
+                .Cast<Season>();
+
+            // This must be the last filter
+            if (!string.IsNullOrEmpty(request.AdjacentTo))
+            {
+                seasons = ItemsService.FilterForAdjacency(seasons, request.AdjacentTo)
+                    .Cast<Season>();
+            }
+
+            var returnItems = seasons.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+                .ToArray();
+
+            return new ItemsResult
+            {
+                TotalRecordCount = returnItems.Length,
+                Items = returnItems
+            };
+        }
+
+        private IEnumerable<Season> FilterVirtualSeasons(GetSeasons request, IEnumerable<Season> items)
+        {
+            if (request.IsMissing.HasValue && request.IsVirtualUnaired.HasValue)
+            {
+                var isMissing = request.IsMissing.Value;
+                var isVirtualUnaired = request.IsVirtualUnaired.Value;
+
+                if (!isMissing && !isVirtualUnaired)
+                {
+                    return items.Where(i => !i.IsMissingOrVirtualUnaired);
+                }
+            }
+
+            if (request.IsMissing.HasValue)
+            {
+                var val = request.IsMissing.Value;
+                items = items.Where(i => i.IsMissingSeason == val);
+            }
+
+            if (request.IsVirtualUnaired.HasValue)
+            {
+                var val = request.IsVirtualUnaired.Value;
+                items = items.Where(i => i.IsVirtualUnaired == val);
+            }
+
+            return items;
+        }
+        
+        public object Get(GetEpisodes request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var series = _libraryManager.GetItemById(request.Id) as Series;
+
+            var fields = request.GetItemFields().ToList();
+
+            var episodes = series.GetRecursiveChildren(user)
+                .OfType<Episode>();
+
+            var sortOrder = ItemSortBy.SortName;
+
+            if (!string.IsNullOrEmpty(request.SeasonId))
+            {
+                var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season;
+
+                if (season.IndexNumber.HasValue)
+                {
+                    episodes = FilterEpisodesBySeason(episodes, season.IndexNumber.Value, true);
+
+                    sortOrder = ItemSortBy.AiredEpisodeOrder;
+                }
+                else
+                {
+                    episodes = season.RecursiveChildren.OfType<Episode>();
+
+                    sortOrder = ItemSortBy.SortName;
+                }
+            }
+
+            else if (request.Season.HasValue)
+            {
+                episodes = FilterEpisodesBySeason(episodes, request.Season.Value, true);
+
+                sortOrder = ItemSortBy.AiredEpisodeOrder;
+            }
+
+            var config = user.Configuration;
+
+            if (!config.DisplayMissingEpisodes)
+            {
+                episodes = episodes.Where(i => !i.IsMissingEpisode);
+            }
+            if (!config.DisplayUnairedEpisodes)
+            {
+                episodes = episodes.Where(i => !i.IsVirtualUnaired);
+            }
+
+            if (request.IsMissing.HasValue)
+            {
+                var val = request.IsMissing.Value;
+                episodes = episodes.Where(i => i.IsMissingEpisode == val);
+            }
+
+            if (request.IsVirtualUnaired.HasValue)
+            {
+                var val = request.IsVirtualUnaired.Value;
+                episodes = episodes.Where(i => i.IsVirtualUnaired == val);
+            }
+
+            episodes = _libraryManager.Sort(episodes, user, new[] { sortOrder }, SortOrder.Ascending)
+                .Cast<Episode>();
+
+            // This must be the last filter
+            if (!string.IsNullOrEmpty(request.AdjacentTo))
+            {
+                episodes = ItemsService.FilterForAdjacency(episodes, request.AdjacentTo)
+                    .Cast<Episode>();
+            }
+
+            var returnItems = episodes.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+                .ToArray();
+
+            return new ItemsResult
+            {
+                TotalRecordCount = returnItems.Length,
+                Items = returnItems
+            };
+        }
+
+        internal static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
+        {
+            if (!includeSpecials || seasonNumber < 1)
+            {
+                return episodes.Where(i => (i.PhysicalSeasonNumber ?? -1) == seasonNumber);
+            }
+
+            return episodes.Where(i =>
+            {
+                var episode = i;
+
+                if (episode != null)
+                {
+                    var currentSeasonNumber = episode.AiredSeasonNumber;
+
+                    return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
+                }
+
+                return false;
+            });
+        }
     }
 }

+ 3 - 18
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -43,7 +43,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class ArtistsService
     /// </summary>
-    public class ArtistsService : BaseItemsByNameService<Artist>
+    public class ArtistsService : BaseItemsByNameService<MusicArtist>
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="ArtistsService" /> class.
@@ -109,24 +109,9 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<Artist> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
+        protected override IEnumerable<MusicArtist> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
-            var itemsList = items.OfType<Audio>().ToList();
-
-            return itemsList
-                .SelectMany(i =>
-                {
-                    var list = new List<string>();
-
-                    if (!string.IsNullOrEmpty(i.AlbumArtist))
-                    {
-                        list.Add(i.AlbumArtist);
-                    }
-                    list.AddRange(i.Artists);
-
-                    return list;
-                })
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+            return LibraryManager.GetAllArtists(items)
                 .Select(name => LibraryManager.GetArtist(name));
         }
     }

+ 1 - 27
MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

@@ -7,7 +7,7 @@ using System;
 
 namespace MediaBrowser.Api.UserLibrary
 {
-    public abstract class BaseItemsRequest
+    public abstract class BaseItemsRequest : IHasItemFields
     {
         /// <summary>
         /// Skips over a given number of items within the results. Use for paging.
@@ -109,32 +109,6 @@ namespace MediaBrowser.Api.UserLibrary
             return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true));
         }
 
-        /// <summary>
-        /// Gets the item fields.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        public IEnumerable<ItemFields> GetItemFields()
-        {
-            var val = Fields;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return new ItemFields[] { };
-            }
-
-            return val.Split(',').Select(v =>
-            {
-                ItemFields value;
-
-                if (Enum.TryParse(v, true, out value))
-                {
-                    return (ItemFields?)value;
-                }
-                return null;
-
-            }).Where(i => i.HasValue).Select(i => i.Value);
-        }
-
         /// <summary>
         /// Gets the image types.
         /// </summary>

+ 160 - 34
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -1,10 +1,13 @@
-using MediaBrowser.Controller.Dto;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
@@ -205,6 +208,27 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? AiredDuringSeason { get; set; }
+
+        [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string MinPremiereDate { get; set; }
+
+        [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string MaxPremiereDate { get; set; }
+
+        [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? HasOverview { get; set; }
+
+        [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? HasImdbId { get; set; }
+
+        [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? HasTmdbId { get; set; }
+
+        [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? HasTvdbId { get; set; }
+        
+        [ApiMember(Name = "IsYearMismatched", Description = "Optional filter by items that are potentially misidentified.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsYearMismatched { get; set; }
     }
 
     /// <summary>
@@ -286,6 +310,12 @@ namespace MediaBrowser.Api.UserLibrary
 
             items = ApplySortOrder(request, items, user, _libraryManager);
 
+            // This must be the last filter
+            if (!string.IsNullOrEmpty(request.AdjacentTo))
+            {
+                items = FilterForAdjacency(items, request.AdjacentTo);
+            }
+
             var itemsArray = items.ToList();
 
             var pagedItems = ApplyPaging(request, itemsArray);
@@ -642,30 +672,6 @@ namespace MediaBrowser.Api.UserLibrary
                 });
             }
 
-            if (!string.IsNullOrEmpty(request.AdjacentTo))
-            {
-                var item = _dtoService.GetItemByDtoId(request.AdjacentTo);
-
-                var allSiblings = item.Parent.GetChildren(user, true).OrderBy(i => i.SortName).ToList();
-
-                var index = allSiblings.IndexOf(item);
-
-                var previousId = Guid.Empty;
-                var nextId = Guid.Empty;
-
-                if (index > 0)
-                {
-                    previousId = allSiblings[index - 1].Id;
-                }
-
-                if (index < allSiblings.Count - 1)
-                {
-                    nextId = allSiblings[index + 1].Id;
-                }
-
-                items = items.Where(i => i.Id == previousId || i.Id == nextId);
-            }
-
             // Min index number
             if (request.MinIndexNumber.HasValue)
             {
@@ -861,7 +867,19 @@ namespace MediaBrowser.Api.UserLibrary
 
             if (request.HasTrailer.HasValue)
             {
-                items = items.Where(i => request.HasTrailer.Value ? i.LocalTrailerIds.Count > 0 : i.LocalTrailerIds.Count == 0);
+                var val = request.HasTrailer.Value;
+                items = items.Where(i =>
+                {
+                    var trailerCount = 0;
+
+                    var hasTrailers = i as IHasTrailers;
+                    if (hasTrailers != null)
+                    {
+                        trailerCount = hasTrailers.LocalTrailerIds.Count;
+                    }
+
+                    return val ? trailerCount > 0 : trailerCount == 0;
+                });
             }
 
             if (request.HasThemeSong.HasValue)
@@ -1005,26 +1023,134 @@ namespace MediaBrowser.Api.UserLibrary
 
             if (request.AiredDuringSeason.HasValue)
             {
-                var val = request.AiredDuringSeason.Value;
+                items = TvShowsService.FilterEpisodesBySeason(items.OfType<Episode>(), request.AiredDuringSeason.Value, true);
+            }
+
+            if (!string.IsNullOrEmpty(request.MinPremiereDate))
+            {
+                var date = DateTime.ParseExact(request.MinPremiereDate, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
+
+                items = items.Where(i => i.PremiereDate.HasValue && i.PremiereDate.Value >= date);
+            }
+
+            if (!string.IsNullOrEmpty(request.MaxPremiereDate))
+            {
+                var date = DateTime.ParseExact(request.MaxPremiereDate, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
+
+                items = items.Where(i => i.PremiereDate.HasValue && i.PremiereDate.Value <= date);
+            }
+
+            if (request.HasOverview.HasValue)
+            {
+                var filterValue = request.HasOverview.Value;
 
                 items = items.Where(i =>
                 {
-                    var episode = i as Episode;
+                    var hasValue = !string.IsNullOrEmpty(i.Overview);
 
-                    if (episode != null)
-                    {
-                        var seasonNumber = episode.AirsAfterSeasonNumber ?? episode.AirsBeforeEpisodeNumber ?? episode.ParentIndexNumber;
+                    return hasValue == filterValue;
+                });
+            }
 
-                        return episode.PremiereDate.HasValue && seasonNumber.HasValue && seasonNumber.Value == val;
-                    }
+            if (request.HasImdbId.HasValue)
+            {
+                var filterValue = request.HasImdbId.Value;
 
-                    return false;
+                items = items.Where(i =>
+                {
+                    var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Imdb));
+
+                    return hasValue == filterValue;
                 });
             }
 
+            if (request.HasTmdbId.HasValue)
+            {
+                var filterValue = request.HasTmdbId.Value;
+
+                items = items.Where(i =>
+                {
+                    var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tmdb));
+
+                    return hasValue == filterValue;
+                });
+            }
+
+            if (request.HasTvdbId.HasValue)
+            {
+                var filterValue = request.HasTvdbId.Value;
+
+                items = items.Where(i =>
+                {
+                    var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb));
+
+                    return hasValue == filterValue;
+                });
+            }
+
+            if (request.IsYearMismatched.HasValue)
+            {
+                var filterValue = request.IsYearMismatched.Value;
+
+                items = items.Where(i => IsYearMismatched(i) == filterValue);
+            }
+
             return items;
         }
 
+        private bool IsYearMismatched(BaseItem item)
+        {
+            if (item.ProductionYear.HasValue)
+            {
+                var path = item.Path;
+
+                if (!string.IsNullOrEmpty(path))
+                {
+                    int? yearInName;
+                    string name;
+                    NameParser.ParseName(Path.GetFileName(path), out name, out yearInName);
+
+                    // Go up a level if we didn't get a year
+                    if (!yearInName.HasValue)
+                    {
+                        NameParser.ParseName(Path.GetFileName(Path.GetDirectoryName(path)), out name, out yearInName);
+                    }
+
+                    if (yearInName.HasValue)
+                    {
+                        return yearInName.Value != item.ProductionYear.Value;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        internal static IEnumerable<BaseItem> FilterForAdjacency(IEnumerable<BaseItem> items, string adjacentToId)
+        {
+            var list = items.ToList();
+
+            var adjacentToIdGuid = new Guid(adjacentToId);
+            var adjacentToItem = list.FirstOrDefault(i => i.Id == adjacentToIdGuid);
+
+            var index = list.IndexOf(adjacentToItem);
+
+            var previousId = Guid.Empty;
+            var nextId = Guid.Empty;
+
+            if (index > 0)
+            {
+                previousId = list[index - 1].Id;
+            }
+
+            if (index < list.Count - 1)
+            {
+                nextId = list[index + 1].Id;
+            }
+
+            return list.Where(i => i.Id == previousId || i.Id == nextId);
+        }
+
         /// <summary>
         /// Determines whether the specified item has image.
         /// </summary>

+ 1 - 1
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -155,7 +155,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>IEnumerable{PersonInfo}.</returns>
         private IEnumerable<PersonInfo> GetAllPeople(IEnumerable<BaseItem> itemsList, string[] personTypes)
         {
-            var people = itemsList.SelectMany(i => i.People.OrderBy(p => p.Type));
+            var people = itemsList.SelectMany(i => i.People.OrderBy(p => p.SortOrder ?? int.MaxValue).ThenBy(p => p.Type));
 
             return personTypes.Length == 0 ?
 

+ 9 - 1
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -489,7 +489,15 @@ namespace MediaBrowser.Api.UserLibrary
             // Get everything
             var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
 
-            var dtos = item.LocalTrailerIds
+            var trailerIds = new List<Guid>();
+
+            var hasTrailers = item as IHasTrailers;
+            if (hasTrailers != null)
+            {
+                trailerIds = hasTrailers.LocalTrailerIds;
+            }
+
+            var dtos = trailerIds
                 .Select(_libraryManager.GetItemById)
                 .OrderBy(i => i.SortName)
                 .Select(i => _dtoService.GetBaseItemDto(i, fields, user, item));

+ 2 - 2
MediaBrowser.Api/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
   <package id="morelinq" version="1.0.16006" targetFramework="net45" />
-  <package id="ServiceStack.Common" version="3.9.62" targetFramework="net45" />
-  <package id="ServiceStack.Text" version="3.9.62" targetFramework="net45" />
+  <package id="ServiceStack.Common" version="3.9.70" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.70" targetFramework="net45" />
 </packages>

+ 13 - 9
MediaBrowser.Common.Implementations/BaseApplicationHost.cs

@@ -22,7 +22,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Net;
-using System.Net.Http;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
@@ -34,7 +33,7 @@ namespace MediaBrowser.Common.Implementations
     /// </summary>
     /// <typeparam name="TApplicationPathsType">The type of the T application paths type.</typeparam>
     public abstract class BaseApplicationHost<TApplicationPathsType> : IApplicationHost
-        where TApplicationPathsType : class, IApplicationPaths, new()
+        where TApplicationPathsType : class, IApplicationPaths
     {
         /// <summary>
         /// Occurs when [has pending restart changed].
@@ -84,7 +83,7 @@ namespace MediaBrowser.Common.Implementations
         /// <summary>
         /// The json serializer
         /// </summary>
-        public readonly IJsonSerializer JsonSerializer = new JsonSerializer();
+        public IJsonSerializer JsonSerializer { get; private set; }
 
         /// <summary>
         /// The _XML serializer
@@ -154,7 +153,7 @@ namespace MediaBrowser.Common.Implementations
         protected IInstallationManager InstallationManager { get; private set; }
 
         protected IFileSystem FileSystemManager { get; private set; }
-        
+
         /// <summary>
         /// Gets or sets the zip client.
         /// </summary>
@@ -182,6 +181,8 @@ namespace MediaBrowser.Common.Implementations
         /// <returns>Task.</returns>
         public virtual async Task Init()
         {
+            JsonSerializer = CreateJsonSerializer();
+
             IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted;
 
             Logger = LogManager.GetLogger("App");
@@ -213,19 +214,24 @@ namespace MediaBrowser.Common.Implementations
 
         }
 
+        protected virtual IJsonSerializer CreateJsonSerializer()
+        {
+            return new JsonSerializer();
+        }
+
         private void SetHttpLimit()
         {
             try
             {
                 // Increase the max http request limit
-                ServicePointManager.DefaultConnectionLimit = Math.Max(48, ServicePointManager.DefaultConnectionLimit);
+                ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
             }
             catch (Exception ex)
             {
                 Logger.ErrorException("Error setting http limit", ex);
             }
         }
-        
+
         /// <summary>
         /// Installs the iso mounters.
         /// </summary>
@@ -353,7 +359,7 @@ namespace MediaBrowser.Common.Implementations
                 FileSystemManager = CreateFileSystemManager();
                 RegisterSingleInstance(FileSystemManager);
 
-                HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, Logger, CreateHttpClient, FileSystemManager);
+                HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, Logger, FileSystemManager);
                 RegisterSingleInstance(HttpClient);
 
                 NetworkManager = CreateNetworkManager();
@@ -378,8 +384,6 @@ namespace MediaBrowser.Common.Implementations
             return new CommonFileSystem(Logger, true);
         }
 
-        protected abstract HttpClient CreateHttpClient(bool enableHttpCompression);
-
         /// <summary>
         /// Gets a list of types within an assembly
         /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference

+ 6 - 4
MediaBrowser.Common.Implementations/BaseApplicationPaths.cs

@@ -20,21 +20,23 @@ namespace MediaBrowser.Common.Implementations
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class.
         /// </summary>
-        /// <param name="useDebugPath">if set to <c>true</c> [use debug paths].</param>
-        protected BaseApplicationPaths(bool useDebugPath)
+        protected BaseApplicationPaths(bool useDebugPath, string applicationPath)
         {
             _useDebugPath = useDebugPath;
+            ApplicationPath = applicationPath;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
         /// </summary>
-        /// <param name="programDataPath">The program data path.</param>
-        protected BaseApplicationPaths(string programDataPath)
+        protected BaseApplicationPaths(string programDataPath, string applicationPath)
         {
             _programDataPath = programDataPath;
+            ApplicationPath = applicationPath;
         }
 
+        public string ApplicationPath { get; private set; }
+
         /// <summary>
         /// The _program data path
         /// </summary>

+ 0 - 6
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientInfo.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Net.Http;
 
 namespace MediaBrowser.Common.Implementations.HttpClientManager
 {
@@ -8,11 +7,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
     /// </summary>
     public class HttpClientInfo
     {
-        /// <summary>
-        /// Gets or sets the HTTP client.
-        /// </summary>
-        /// <value>The HTTP client.</value>
-        public HttpClient HttpClient { get; set; }
         /// <summary>
         /// Gets or sets the last timeout.
         /// </summary>

+ 274 - 248
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -9,7 +9,10 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net;
+using System.Net.Cache;
 using System.Net.Http;
+using System.Reflection;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -21,6 +24,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
     /// </summary>
     public class HttpClientManager : IHttpClient
     {
+        /// <summary>
+        /// When one request to a host times out, we'll ban all other requests for this period of time, to prevent scans from stalling
+        /// </summary>
+        private const int TimeoutSeconds = 30;
+
         /// <summary>
         /// The _logger
         /// </summary>
@@ -31,23 +39,18 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         /// </summary>
         private readonly IApplicationPaths _appPaths;
 
-        public delegate HttpClient GetHttpClientHandler(bool enableHttpCompression);
-
-        private readonly GetHttpClientHandler _getHttpClientHandler;
         private readonly IFileSystem _fileSystem;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="HttpClientManager"/> class.
+        /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="logger">The logger.</param>
-        /// <param name="getHttpClientHandler">The get HTTP client handler.</param>
-        /// <exception cref="System.ArgumentNullException">
-        /// appPaths
+        /// <param name="fileSystem">The file system.</param>
+        /// <exception cref="System.ArgumentNullException">appPaths
         /// or
-        /// logger
-        /// </exception>
-        public HttpClientManager(IApplicationPaths appPaths, ILogger logger, GetHttpClientHandler getHttpClientHandler, IFileSystem fileSystem)
+        /// logger</exception>
+        public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
         {
             if (appPaths == null)
             {
@@ -59,7 +62,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             }
 
             _logger = logger;
-            _getHttpClientHandler = getHttpClientHandler;
             _fileSystem = fileSystem;
             _appPaths = appPaths;
         }
@@ -91,111 +93,83 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             if (!_httpClients.TryGetValue(key, out client))
             {
-                client = new HttpClientInfo
-                {
+                client = new HttpClientInfo();
 
-                    HttpClient = _getHttpClientHandler(enableHttpCompression)
-                };
                 _httpClients.TryAdd(key, client);
             }
 
             return client;
         }
 
-        public async Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
+        private WebRequest GetMonoRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
         {
-            ValidateParams(options.Url, options.CancellationToken);
+            var request = WebRequest.Create(options.Url);
 
-            options.CancellationToken.ThrowIfCancellationRequested();
-
-            var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
-
-            if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
+            if (!string.IsNullOrEmpty(options.AcceptHeader))
             {
-                throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) { IsTimedOut = true };
+                request.Headers.Add("Accept", options.AcceptHeader);
             }
 
-            using (var message = GetHttpRequestMessage(options))
-            {
-                if (options.ResourcePool != null)
-                {
-                    await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
-                }
-
-                if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
-                {
-                    if (options.ResourcePool != null)
-                    {
-                        options.ResourcePool.Release();
-                    }
-
-                    throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true };
-                }
-
-                _logger.Info("HttpClientManager.Get url: {0}", options.Url);
-
-                try
-                {
-                    options.CancellationToken.ThrowIfCancellationRequested();
-
-                    var response = await client.HttpClient.SendAsync(message, HttpCompletionOption.ResponseContentRead, options.CancellationToken).ConfigureAwait(false);
-
-                    EnsureSuccessStatusCode(response);
-
-                    options.CancellationToken.ThrowIfCancellationRequested();
-
-                    return new HttpResponseInfo
-                    {
-                        Content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+            request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
+            request.ConnectionGroupName = GetHostFromUrl(options.Url);
+            request.Method = method;
+            request.Timeout = 20000;
 
-                        StatusCode = response.StatusCode,
+            if (!string.IsNullOrEmpty(options.UserAgent))
+            {
+                request.Headers.Add("User-Agent", options.UserAgent);
+            }
 
-                        ContentType = response.Content.Headers.ContentType.MediaType
-                    };
-                }
-                catch (OperationCanceledException ex)
-                {
-                    var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
+            return request;
+        }
 
-                    var httpException = exception as HttpException;
+        private PropertyInfo _httpBehaviorPropertyInfo;
+        private WebRequest GetRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
+        {
+#if __MonoCS__
+            return GetMonoRequest(options, method, enableHttpCompression);
+#endif
+            
+            var request = HttpWebRequest.CreateHttp(options.Url);
 
-                    if (httpException != null && httpException.IsTimedOut)
-                    {
-                        client.LastTimeout = DateTime.UtcNow;
-                    }
+            if (!string.IsNullOrEmpty(options.AcceptHeader))
+            {
+                request.Accept = options.AcceptHeader;
+            }
 
-                    throw exception;
-                }
-                catch (HttpRequestException ex)
-                {
-                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+            request.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
+            request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
+            request.ConnectionGroupName = GetHostFromUrl(options.Url);
+            request.KeepAlive = true;
+            request.Method = method;
+            request.Pipelined = true;
+            request.Timeout = 20000;
 
-                    throw new HttpException(ex.Message, ex);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+            if (!string.IsNullOrEmpty(options.UserAgent))
+            {
+                request.UserAgent = options.UserAgent;
+            }
 
-                    throw;
-                }
-                finally
-                {
-                    if (options.ResourcePool != null)
-                    {
-                        options.ResourcePool.Release();
-                    }
-                }
+            // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
+            // May need to remove this for mono
+            var sp = request.ServicePoint;
+            if (_httpBehaviorPropertyInfo == null)
+            {
+                _httpBehaviorPropertyInfo = sp.GetType().GetProperty("HttpBehaviour", BindingFlags.Instance | BindingFlags.NonPublic);
             }
+            _httpBehaviorPropertyInfo.SetValue(sp, (byte)0, null);
+
+            return request;
         }
 
         /// <summary>
-        /// Performs a GET request and returns the resulting stream
+        /// Gets the response internal.
         /// </summary>
         /// <param name="options">The options.</param>
-        /// <returns>Task{Stream}.</returns>
-        /// <exception cref="HttpException"></exception>
-        /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
-        public async Task<Stream> Get(HttpRequestOptions options)
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        /// <exception cref="HttpException">
+        /// </exception>
+        public async Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
         {
             ValidateParams(options.Url, options.CancellationToken);
 
@@ -203,77 +177,115 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
 
-            if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
+            if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
             {
                 throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) { IsTimedOut = true };
             }
 
-            using (var message = GetHttpRequestMessage(options))
+            var httpWebRequest = GetRequest(options, "GET", options.EnableHttpCompression);
+
+            if (options.ResourcePool != null)
+            {
+                await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
+            }
+
+            if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
             {
                 if (options.ResourcePool != null)
                 {
-                    await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
+                    options.ResourcePool.Release();
                 }
 
-                if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
-                {
-                    if (options.ResourcePool != null)
-                    {
-                        options.ResourcePool.Release();
-                    }
+                throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true };
+            }
 
-                    throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true };
-                }
+            _logger.Info("HttpClientManager.GET url: {0}", options.Url);
 
-                _logger.Info("HttpClientManager.Get url: {0}", options.Url);
+            try
+            {
+                options.CancellationToken.ThrowIfCancellationRequested();
 
-                try
+                using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
                 {
+                    var httpResponse = (HttpWebResponse)response;
+
+                    EnsureSuccessStatusCode(httpResponse);
+
                     options.CancellationToken.ThrowIfCancellationRequested();
 
-                    var response = await client.HttpClient.SendAsync(message, HttpCompletionOption.ResponseContentRead, options.CancellationToken).ConfigureAwait(false);
+                    using (var stream = httpResponse.GetResponseStream())
+                    {
+                        var memoryStream = new MemoryStream();
 
-                    EnsureSuccessStatusCode(response);
+                        await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
 
-                    options.CancellationToken.ThrowIfCancellationRequested();
+                        memoryStream.Position = 0;
 
-                    return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-                }
-                catch (OperationCanceledException ex)
-                {
-                    var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
+                        return new HttpResponseInfo
+                        {
+                            Content = memoryStream,
 
-                    var httpException = exception as HttpException;
+                            StatusCode = httpResponse.StatusCode,
 
-                    if (httpException != null && httpException.IsTimedOut)
-                    {
-                        client.LastTimeout = DateTime.UtcNow;
+                            ContentType = httpResponse.ContentType
+                        };
                     }
-
-                    throw exception;
                 }
-                catch (HttpRequestException ex)
-                {
-                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+            }
+            catch (OperationCanceledException ex)
+            {
+                var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
 
-                    throw new HttpException(ex.Message, ex);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+                var httpException = exception as HttpException;
 
-                    throw;
+                if (httpException != null && httpException.IsTimedOut)
+                {
+                    client.LastTimeout = DateTime.UtcNow;
                 }
-                finally
+
+                throw exception;
+            }
+            catch (HttpRequestException ex)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                throw new HttpException(ex.Message, ex);
+            }
+            catch (WebException ex)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                throw new HttpException(ex.Message, ex);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                throw;
+            }
+            finally
+            {
+                if (options.ResourcePool != null)
                 {
-                    if (options.ResourcePool != null)
-                    {
-                        options.ResourcePool.Release();
-                    }
+                    options.ResourcePool.Release();
                 }
             }
         }
 
+        /// <summary>
+        /// Performs a GET request and returns the resulting stream
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{Stream}.</returns>
+        /// <exception cref="HttpException"></exception>
+        /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+        public async Task<Stream> Get(HttpRequestOptions options)
+        {
+            var response = await GetResponse(options).ConfigureAwait(false);
+
+            return response.Content;
+        }
+
         /// <summary>
         /// Performs a GET request and returns the resulting stream
         /// </summary>
@@ -305,64 +317,112 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         /// <summary>
         /// Performs a POST request
         /// </summary>
-        /// <param name="url">The URL.</param>
+        /// <param name="options">The options.</param>
         /// <param name="postData">Params to add to the POST data.</param>
-        /// <param name="resourcePool">The resource pool.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>stream on success, null on failure</returns>
+        /// <exception cref="HttpException">
+        /// </exception>
         /// <exception cref="System.ArgumentNullException">postData</exception>
         /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
-        public async Task<Stream> Post(string url, Dictionary<string, string> postData, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+        public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData)
         {
-            ValidateParams(url, cancellationToken);
+            ValidateParams(options.Url, options.CancellationToken);
 
-            if (postData == null)
-            {
-                throw new ArgumentNullException("postData");
-            }
+            options.CancellationToken.ThrowIfCancellationRequested();
 
-            cancellationToken.ThrowIfCancellationRequested();
+            var httpWebRequest = GetRequest(options, "POST", options.EnableHttpCompression);
 
             var strings = postData.Keys.Select(key => string.Format("{0}={1}", key, postData[key]));
             var postContent = string.Join("&", strings.ToArray());
-            var content = new StringContent(postContent, Encoding.UTF8, "application/x-www-form-urlencoded");
+            var bytes = Encoding.UTF8.GetBytes(postContent);
 
-            if (resourcePool != null)
+            httpWebRequest.ContentType = "application/x-www-form-urlencoded";
+            httpWebRequest.ContentLength = bytes.Length;
+            httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length);
+
+            if (options.ResourcePool != null)
             {
-                await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+                await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
             }
 
-            _logger.Info("HttpClientManager.Post url: {0}", url);
+            _logger.Info("HttpClientManager.POST url: {0}", options.Url);
 
             try
             {
-                cancellationToken.ThrowIfCancellationRequested();
+                options.CancellationToken.ThrowIfCancellationRequested();
 
-                var msg = await GetHttpClient(GetHostFromUrl(url), true).HttpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
+                using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
+                {
+                    var httpResponse = (HttpWebResponse)response;
+
+                    EnsureSuccessStatusCode(httpResponse);
 
-                EnsureSuccessStatusCode(msg);
+                    options.CancellationToken.ThrowIfCancellationRequested();
 
-                return await msg.Content.ReadAsStreamAsync().ConfigureAwait(false);
+                    using (var stream = httpResponse.GetResponseStream())
+                    {
+                        var memoryStream = new MemoryStream();
+
+                        await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+
+                        memoryStream.Position = 0;
+
+                        return memoryStream;
+                    }
+                }
             }
             catch (OperationCanceledException ex)
             {
-                throw GetCancellationException(url, cancellationToken, ex);
+                var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
+
+                throw exception;
             }
             catch (HttpRequestException ex)
             {
-                _logger.ErrorException("Error getting response from " + url, ex);
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                throw new HttpException(ex.Message, ex);
+            }
+            catch (WebException ex)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
 
                 throw new HttpException(ex.Message, ex);
             }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                throw;
+            }
             finally
             {
-                if (resourcePool != null)
+                if (options.ResourcePool != null)
                 {
-                    resourcePool.Release();
+                    options.ResourcePool.Release();
                 }
             }
         }
 
+        /// <summary>
+        /// Performs a POST request
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <param name="postData">Params to add to the POST data.</param>
+        /// <param name="resourcePool">The resource pool.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>stream on success, null on failure</returns>
+        public Task<Stream> Post(string url, Dictionary<string, string> postData, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+        {
+            return Post(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = resourcePool,
+                CancellationToken = cancellationToken
+
+            }, postData);
+        }
+
         /// <summary>
         /// Downloads the contents of a given url into a temporary location
         /// </summary>
@@ -391,6 +451,8 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             options.CancellationToken.ThrowIfCancellationRequested();
 
+            var httpWebRequest = GetRequest(options, "GET", options.EnableHttpCompression);
+
             if (options.ResourcePool != null)
             {
                 await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
@@ -398,57 +460,68 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             options.Progress.Report(0);
 
-            _logger.Info("HttpClientManager.GetTempFile url: {0}, temp file: {1}", options.Url, tempFile);
+            _logger.Info("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
 
             try
             {
                 options.CancellationToken.ThrowIfCancellationRequested();
 
-                using (var message = GetHttpRequestMessage(options))
+                using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
                 {
-                    using (var response = await GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression).HttpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false))
-                    {
-                        EnsureSuccessStatusCode(response);
+                    var httpResponse = (HttpWebResponse)response;
 
-                        options.CancellationToken.ThrowIfCancellationRequested();
+                    EnsureSuccessStatusCode(httpResponse);
+
+                    options.CancellationToken.ThrowIfCancellationRequested();
 
-                        var contentLength = GetContentLength(response);
+                    var contentLength = GetContentLength(httpResponse);
 
-                        if (!contentLength.HasValue)
+                    if (!contentLength.HasValue)
+                    {
+                        // We're not able to track progress
+                        using (var stream = httpResponse.GetResponseStream())
                         {
-                            // We're not able to track progress
-                            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+                            using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true))
                             {
-                                using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true))
-                                {
-                                    await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
-                                }
+                                await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
                             }
                         }
-                        else
+                    }
+                    else
+                    {
+                        using (var stream = ProgressStream.CreateReadProgressStream(httpResponse.GetResponseStream(), options.Progress.Report, contentLength.Value))
                         {
-                            using (var stream = ProgressStream.CreateReadProgressStream(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), options.Progress.Report, contentLength.Value))
+                            using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true))
                             {
-                                using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true))
-                                {
-                                    await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
-                                }
+                                await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
                             }
                         }
+                    }
 
-                        options.Progress.Report(100);
+                    options.Progress.Report(100);
 
-                        return new HttpResponseInfo
-                        {
-                            TempFilePath = tempFile,
+                    return new HttpResponseInfo
+                    {
+                        TempFilePath = tempFile,
 
-                            StatusCode = response.StatusCode,
+                        StatusCode = httpResponse.StatusCode,
 
-                            ContentType = response.Content.Headers.ContentType.MediaType
-                        };
-                    }
+                        ContentType = httpResponse.ContentType
+                    };
                 }
             }
+            catch (OperationCanceledException ex)
+            {
+                throw GetTempFileException(ex, options, tempFile);
+            }
+            catch (HttpRequestException ex)
+            {
+                throw GetTempFileException(ex, options, tempFile);
+            }
+            catch (WebException ex)
+            {
+                throw GetTempFileException(ex, options, tempFile);
+            }
             catch (Exception ex)
             {
                 throw GetTempFileException(ex, options, tempFile);
@@ -462,63 +535,16 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             }
         }
 
-        /// <summary>
-        /// Gets the message.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <returns>HttpResponseMessage.</returns>
-        private HttpRequestMessage GetHttpRequestMessage(HttpRequestOptions options)
+        private long? GetContentLength(HttpWebResponse response)
         {
-            var message = new HttpRequestMessage(HttpMethod.Get, options.Url);
+            var length = response.ContentLength;
 
-            foreach (var pair in options.RequestHeaders.ToList())
-            {
-                if (!message.Headers.TryAddWithoutValidation(pair.Key, pair.Value))
-                {
-                    _logger.Error("Unable to add request header {0} with value {1}", pair.Key, pair.Value);
-                }
-            }
-
-            return message;
-        }
-
-        /// <summary>
-        /// Gets the length of the content.
-        /// </summary>
-        /// <param name="response">The response.</param>
-        /// <returns>System.Nullable{System.Int64}.</returns>
-        private long? GetContentLength(HttpResponseMessage response)
-        {
-            IEnumerable<string> lengthValues = null;
-
-            // Seeing some InvalidOperationException here under mono
-            try
-            {
-                response.Headers.TryGetValues("content-length", out lengthValues);
-            }
-            catch (InvalidOperationException ex)
-            {
-                _logger.ErrorException("Error accessing response.Headers.TryGetValues Content-Length", ex);
-            }
-
-            if (lengthValues == null)
-            {
-                try
-                {
-                    response.Content.Headers.TryGetValues("content-length", out lengthValues);
-                }
-                catch (InvalidOperationException ex)
-                {
-                    _logger.ErrorException("Error accessing response.Content.Headers.TryGetValues Content-Length", ex);
-                }
-            }
-
-            if (lengthValues == null)
+            if (length == 0)
             {
                 return null;
             }
 
-            return long.Parse(string.Join(string.Empty, lengthValues.ToArray()), UsCulture);
+            return length;
         }
 
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
@@ -545,16 +571,23 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             _logger.ErrorException("Error getting response from " + options.Url, ex);
 
-            var httpRequestException = ex as HttpRequestException;
-
             // Cleanup
             DeleteTempFile(tempFile);
 
+            var httpRequestException = ex as HttpRequestException;
+
             if (httpRequestException != null)
             {
                 return new HttpException(ex.Message, ex);
             }
 
+            var webException = ex as WebException;
+
+            if (webException != null)
+            {
+                return new HttpException(ex.Message, ex);
+            }
+            
             return ex;
         }
 
@@ -613,11 +646,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         {
             if (dispose)
             {
-                foreach (var client in _httpClients.Values.ToList())
-                {
-                    client.HttpClient.Dispose();
-                }
-
                 _httpClients.Clear();
             }
         }
@@ -645,16 +673,14 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             return exception;
         }
 
-        /// <summary>
-        /// Ensures the success status code.
-        /// </summary>
-        /// <param name="response">The response.</param>
-        /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
-        private void EnsureSuccessStatusCode(HttpResponseMessage response)
+        private void EnsureSuccessStatusCode(HttpWebResponse response)
         {
-            if (!response.IsSuccessStatusCode)
+            var statusCode = response.StatusCode;
+            var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
+
+            if (!isSuccessful)
             {
-                throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode };
+                throw new HttpException(response.StatusDescription) { StatusCode = response.StatusCode };
             }
         }
 

+ 4 - 3
MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj

@@ -41,6 +41,10 @@
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\packages\NLog.2.1.0\lib\net45\NLog.dll</HintPath>
     </Reference>
+    <Reference Include="ServiceStack.Text, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.70\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
     <Reference Include="SharpCompress">
       <HintPath>..\packages\sharpcompress.0.10.1.3\lib\net40\SharpCompress.dll</HintPath>
     </Reference>
@@ -55,9 +59,6 @@
     <Reference Include="System.Net" />
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Xml" />
-    <Reference Include="ServiceStack.Text">
-      <HintPath>..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll</HintPath>
-    </Reference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs">

+ 0 - 1
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -29,7 +29,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         /// <summary>
         /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
         /// </summary>
-        /// <param name="appPaths">The app paths.</param>
         public DeleteCacheFileTask(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
         {
             ApplicationPaths = appPaths;

+ 4 - 2
MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs

@@ -80,7 +80,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
 
             using (Stream stream = File.OpenRead(file))
             {
-                return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+                return DeserializeFromStream(stream, type);
             }
         }
 
@@ -101,7 +101,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
 
             using (Stream stream = File.OpenRead(file))
             {
-                return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+                return DeserializeFromStream<T>(stream);
             }
         }
 
@@ -169,6 +169,8 @@ namespace MediaBrowser.Common.Implementations.Serialization
             ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.JsonDateHandler.ISO8601;
             ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
             ServiceStack.Text.JsConfig.IncludeNullValues = false;
+            ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
+            ServiceStack.Text.JsConfig.AssumeUtc = true;
         }
 
         /// <summary>

+ 1 - 1
MediaBrowser.Common.Implementations/Updates/InstallationManager.cs

@@ -168,7 +168,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             {
                 // Let dev users get results more often for testing purposes
                 var cacheLength = _config.CommonConfiguration.SystemUpdateLevel == PackageVersionClass.Dev
-                                      ? TimeSpan.FromMinutes(15)
+                                      ? TimeSpan.FromMinutes(5)
                                       : TimeSpan.FromHours(12);
 
                 if ((DateTime.UtcNow - _lastPackageListResult.Item2) < cacheLength)

+ 1 - 1
MediaBrowser.Common.Implementations/packages.config

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
   <package id="NLog" version="2.1.0" targetFramework="net45" />
-  <package id="ServiceStack.Text" version="3.9.62" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.70" targetFramework="net45" />
   <package id="sharpcompress" version="0.10.1.3" targetFramework="net45" />
   <package id="SimpleInjector" version="2.3.6" targetFramework="net45" />
 </packages>

+ 6 - 0
MediaBrowser.Common/Configuration/IApplicationPaths.cs

@@ -6,6 +6,12 @@ namespace MediaBrowser.Common.Configuration
     /// </summary>
     public interface IApplicationPaths
     {
+        /// <summary>
+        /// Gets the application path.
+        /// </summary>
+        /// <value>The application path.</value>
+        string ApplicationPath { get; }
+
         /// <summary>
         /// Gets the path to the program data folder
         /// </summary>

+ 12 - 9
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -35,18 +35,21 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="System" />
-    <Reference Include="System.Core" />
-    <Reference Include="Microsoft.CSharp" />
-    <Reference Include="ServiceStack.Common">
-      <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll</HintPath>
+    <Reference Include="ServiceStack.Common, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Common.dll</HintPath>
     </Reference>
-    <Reference Include="ServiceStack.Interfaces">
-      <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll</HintPath>
+    <Reference Include="ServiceStack.Interfaces, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Interfaces.dll</HintPath>
     </Reference>
-    <Reference Include="ServiceStack.Text">
-      <HintPath>..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll</HintPath>
+    <Reference Include="ServiceStack.Text, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.70\lib\net35\ServiceStack.Text.dll</HintPath>
     </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="Microsoft.CSharp" />
   </ItemGroup>
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs">

+ 5 - 0
MediaBrowser.Common/Net/IHttpClient.cs

@@ -73,6 +73,11 @@ namespace MediaBrowser.Common.Net
         /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
         Task<string> GetTempFile(HttpRequestOptions options);
 
+        /// <summary>
+        /// Gets the temporary file response.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
         Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options);
     }
 }

+ 5 - 0
MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs

@@ -42,4 +42,9 @@ namespace MediaBrowser.Common.ScheduledTasks
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         IEnumerable<ITaskTrigger> GetDefaultTriggers();
     }
+
+    public interface IConfigurableScheduledTask
+    {
+        bool IsHidden { get; }
+    }
 }

+ 11 - 1
MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs

@@ -16,6 +16,15 @@ namespace MediaBrowser.Common.ScheduledTasks
         /// <returns>TaskInfo.</returns>
         public static TaskInfo GetTaskInfo(IScheduledTaskWorker task)
         {
+            var isHidden = false;
+
+            var configurableTask = task.ScheduledTask as IConfigurableScheduledTask;
+
+            if (configurableTask != null)
+            {
+                isHidden = configurableTask.IsHidden;
+            }
+
             return new TaskInfo
             {
                 Name = task.Name,
@@ -25,7 +34,8 @@ namespace MediaBrowser.Common.ScheduledTasks
                 LastExecutionResult = task.LastExecutionResult,
                 Triggers = task.Triggers.Select(GetTriggerInfo).ToList(),
                 Description = task.Description,
-                Category = task.Category
+                Category = task.Category,
+                IsHidden = isHidden
             };
         }
 

+ 2 - 2
MediaBrowser.Common/packages.config

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="ServiceStack.Common" version="3.9.62" targetFramework="net45" />
-  <package id="ServiceStack.Text" version="3.9.62" targetFramework="net45" />
+  <package id="ServiceStack.Common" version="3.9.70" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.70" targetFramework="net45" />
 </packages>

+ 0 - 86
MediaBrowser.Controller/Entities/Audio/Artist.cs

@@ -1,86 +0,0 @@
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Runtime.Serialization;
-
-namespace MediaBrowser.Controller.Entities.Audio
-{
-    /// <summary>
-    /// Class Artist
-    /// </summary>
-    public class Artist : BaseItem, IItemByName, IHasMusicGenres
-    {
-        public Artist()
-        {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
-        }
-
-        public string LastFmImageUrl { get; set; }
-        public string LastFmImageSize { get; set; }
-
-        /// <summary>
-        /// Gets the user data key.
-        /// </summary>
-        /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
-        {
-            return GetUserDataKey(this);
-        }
-
-        [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
-
-        /// <summary>
-        /// Finds the music artist.
-        /// </summary>
-        /// <param name="artist">The artist.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <returns>MusicArtist.</returns>
-        public static MusicArtist FindMusicArtist(Artist artist, ILibraryManager libraryManager)
-        {
-            return FindMusicArtist(artist, libraryManager.RootFolder.RecursiveChildren.OfType<MusicArtist>());
-        }
-
-        /// <summary>
-        /// Finds the music artist.
-        /// </summary>
-        /// <param name="artist">The artist.</param>
-        /// <param name="allMusicArtists">All music artists.</param>
-        /// <returns>MusicArtist.</returns>
-        public static MusicArtist FindMusicArtist(Artist artist, IEnumerable<MusicArtist> allMusicArtists)
-        {
-            var musicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz);
-
-            return allMusicArtists.FirstOrDefault(i =>
-            {
-                if (!string.IsNullOrWhiteSpace(musicBrainzId) && string.Equals(musicBrainzId, i.GetProviderId(MetadataProviders.Musicbrainz), StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-
-                return string.Compare(i.Name, artist.Name, CultureInfo.CurrentCulture, CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0;
-            });
-        }
-
-        /// <summary>
-        /// Gets the user data key.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        public static string GetUserDataKey(BaseItem item)
-        {
-            var id = item.GetProviderId(MetadataProviders.Musicbrainz);
-
-            if (!string.IsNullOrEmpty(id))
-            {
-                return "Artist-Musicbrainz-" + id;
-            }
-
-            return "Artist-" + item.Name;
-        }
-    }
-}

+ 79 - 3
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -1,11 +1,65 @@
-
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
 namespace MediaBrowser.Controller.Entities.Audio
 {
     /// <summary>
     /// Class MusicArtist
     /// </summary>
-    public class MusicArtist : Folder
+    public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess
     {
+        [IgnoreDataMember]
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
+
+        public bool IsAccessedByName { get; set; }
+
+        public override bool IsFolder
+        {
+            get
+            {
+                return !IsAccessedByName;
+            }
+        }
+
+        protected override IEnumerable<BaseItem> ActualChildren
+        {
+            get
+            {
+                if (IsAccessedByName)
+                {
+                    return new List<BaseItem>();
+                }
+
+                return base.ActualChildren;
+            }
+        }
+
+        protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false)
+        {
+            if (IsAccessedByName)
+            {
+                // Should never get in here anyway
+                return Task.FromResult(true);
+            }
+
+            return base.ValidateChildrenInternal(progress, cancellationToken, recursive, forceRefreshMetadata);
+        }
+
+        public override string GetClientTypeName()
+        {
+            if (IsAccessedByName)
+            {
+                //return "Artist";
+            }
+
+            return base.GetClientTypeName();
+        }
+
         /// <summary>
         /// Gets or sets the last fm image URL.
         /// </summary>
@@ -13,13 +67,35 @@ namespace MediaBrowser.Controller.Entities.Audio
         public string LastFmImageUrl { get; set; }
         public string LastFmImageSize { get; set; }
 
+        public MusicArtist()
+        {
+            UserItemCountList = new List<ItemByNameCounts>();
+        }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Artist.GetUserDataKey(this);
+            return GetUserDataKey(this);
+        }
+
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        public static string GetUserDataKey(BaseItem item)
+        {
+            var id = item.GetProviderId(MetadataProviders.Musicbrainz);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                return "Artist-Musicbrainz-" + id;
+            }
+
+            return "Artist-" + item.Name;
         }
     }
 }

+ 2 - 2
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     {
         public MusicGenre()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         /// <summary>
@@ -25,6 +25,6 @@ namespace MediaBrowser.Controller.Entities.Audio
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
     }
 }

+ 55 - 34
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
@@ -38,10 +37,8 @@ namespace MediaBrowser.Controller.Entities
             Tags = new List<string>();
             ThemeSongIds = new List<Guid>();
             ThemeVideoIds = new List<Guid>();
-            LocalTrailerIds = new List<Guid>();
             LockedFields = new List<MetadataFields>();
             Taglines = new List<string>();
-            RemoteTrailers = new List<MediaUrl>();
             ImageSources = new List<ImageSourceInfo>();
         }
 
@@ -87,30 +84,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The id.</value>
         public Guid Id { get; set; }
 
-        /// <summary>
-        /// Gets or sets the budget.
-        /// </summary>
-        /// <value>The budget.</value>
-        public double? Budget { get; set; }
-
         /// <summary>
         /// Gets or sets the taglines.
         /// </summary>
         /// <value>The taglines.</value>
         public List<string> Taglines { get; set; }
 
-        /// <summary>
-        /// Gets or sets the revenue.
-        /// </summary>
-        /// <value>The revenue.</value>
-        public double? Revenue { get; set; }
-
-        /// <summary>
-        /// Gets or sets the trailer URL.
-        /// </summary>
-        /// <value>The trailer URL.</value>
-        public List<MediaUrl> RemoteTrailers { get; set; }
-
         /// <summary>
         /// Return the id that should be used to key display prefs for this item.
         /// Default is based on the type for everything except actual generic folders.
@@ -139,6 +118,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the type of the location.
         /// </summary>
         /// <value>The type of the location.</value>
+        [IgnoreDataMember]
         public virtual LocationType LocationType
         {
             get
@@ -483,6 +463,22 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         public Folder Parent { get; set; }
 
+        [IgnoreDataMember]
+        public IEnumerable<Folder> Parents
+        {
+            get
+            {
+                var parent = Parent;
+
+                while (parent != null)
+                {
+                    yield return parent;
+
+                    parent = parent.Parent;
+                }
+            }
+        }
+
         /// <summary>
         /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
         /// </summary>
@@ -630,11 +626,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The original run time ticks.</value>
         public long? OriginalRunTimeTicks { get; set; }
         /// <summary>
-        /// Gets or sets the aspect ratio.
-        /// </summary>
-        /// <value>The aspect ratio.</value>
-        public string AspectRatio { get; set; }
-        /// <summary>
         /// Gets or sets the production year.
         /// </summary>
         /// <value>The production year.</value>
@@ -655,7 +646,6 @@ namespace MediaBrowser.Controller.Entities
 
         public List<Guid> ThemeSongIds { get; set; }
         public List<Guid> ThemeVideoIds { get; set; }
-        public List<Guid> LocalTrailerIds { get; set; }
 
         [IgnoreDataMember]
         public virtual string OfficialRatingForComparison
@@ -898,7 +888,11 @@ namespace MediaBrowser.Controller.Entities
 
                 themeVideosChanged = await RefreshThemeVideos(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
 
-                localTrailersChanged = await RefreshLocalTrailers(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                var hasTrailers = this as IHasTrailers;
+                if (hasTrailers != null)
+                {
+                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                }
             }
 
             cancellationToken.ThrowIfCancellationRequested();
@@ -918,18 +912,18 @@ namespace MediaBrowser.Controller.Entities
             return changed;
         }
 
-        private async Task<bool> RefreshLocalTrailers(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
+        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
         {
             var newItems = LoadLocalTrailers().ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
-            var itemsChanged = !LocalTrailerIds.SequenceEqual(newItemIds);
+            var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
 
             var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            LocalTrailerIds = newItemIds;
+            item.LocalTrailerIds = newItemIds;
 
             return itemsChanged || results.Contains(true);
         }
@@ -1134,6 +1128,11 @@ namespace MediaBrowser.Controller.Entities
             return changed;
         }
 
+        public virtual string GetClientTypeName()
+        {
+            return GetType().Name;
+        }
+
         /// <summary>
         /// Determines if the item is considered new based on user settings
         /// </summary>
@@ -1187,6 +1186,7 @@ namespace MediaBrowser.Controller.Entities
                 if (existing != null)
                 {
                     existing.Type = PersonType.GuestStar;
+                    existing.SortOrder = person.SortOrder ?? existing.SortOrder;
                     return;
                 }
             }
@@ -1203,23 +1203,35 @@ namespace MediaBrowser.Controller.Entities
                 else
                 {
                     // Was there, if no role and we have one - fill it in
-                    if (string.IsNullOrWhiteSpace(existing.Role) && !string.IsNullOrWhiteSpace(person.Role)) existing.Role = person.Role;
+                    if (string.IsNullOrWhiteSpace(existing.Role) && !string.IsNullOrWhiteSpace(person.Role))
+                    {
+                        existing.Role = person.Role;
+                    }
+
+                    existing.SortOrder = person.SortOrder ?? existing.SortOrder;
                 }
             }
             else
             {
+                var existing = People.FirstOrDefault(p =>
+                            string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) &&
+                            string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase));
+
                 // Check for dupes based on the combination of Name and Type
-                if (!People.Any(p => string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) && string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase)))
+                if (existing == null)
                 {
                     People.Add(person);
                 }
+                else
+                {
+                    existing.SortOrder = person.SortOrder ?? existing.SortOrder;
+                }
             }
         }
 
         /// <summary>
         /// Adds the tagline.
         /// </summary>
-        /// <param name="item">The item.</param>
         /// <param name="tagline">The tagline.</param>
         /// <exception cref="System.ArgumentNullException">tagline</exception>
         public void AddTagline(string tagline)
@@ -1737,5 +1749,14 @@ namespace MediaBrowser.Controller.Entities
             // See if we can avoid a file system lookup by looking for the file in ResolveArgs
             return metaFileEntry == null ? FileSystem.GetLastWriteTimeUtc(imagePath) : FileSystem.GetLastWriteTimeUtc(metaFileEntry);
         }
+
+        /// <summary>
+        /// Gets the file system path to delete when the item is to be deleted
+        /// </summary>
+        /// <returns></returns>
+        public virtual IEnumerable<string> GetDeletePaths()
+        {
+            return new[] { Path };
+        }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/Extensions.cs

@@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="url">The URL.</param>
         /// <param name="isDirectLink">if set to <c>true</c> [is direct link].</param>
         /// <exception cref="System.ArgumentNullException">url</exception>
-        public static void AddTrailerUrl(this BaseItem item, string url, bool isDirectLink)
+        public static void AddTrailerUrl(this IHasTrailers item, string url, bool isDirectLink)
         {
             if (string.IsNullOrWhiteSpace(url))
             {

+ 18 - 35
MediaBrowser.Controller/Entities/Folder.cs

@@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities
                 item.Id = item.Path.GetMBId(item.GetType());
             }
 
-            if (_children.Any(i => i.Id == item.Id))
+            if (ActualChildren.Any(i => i.Id == item.Id))
             {
                 throw new ArgumentException(string.Format("A child with the Id {0} already exists.", item.Id));
             }
@@ -108,14 +108,14 @@ namespace MediaBrowser.Controller.Entities
 
             await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
 
-            await ItemRepository.SaveChildren(Id, _children.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
+            await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
         }
 
         protected void AddChildrenInternal(IEnumerable<BaseItem> children)
         {
             lock (_childrenSyncLock)
             {
-                var newChildren = _children.ToList();
+                var newChildren = ActualChildren.ToList();
                 newChildren.AddRange(children);
                 _children = newChildren;
             }
@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities
         {
             lock (_childrenSyncLock)
             {
-                var newChildren = _children.ToList();
+                var newChildren = ActualChildren.ToList();
                 newChildren.Add(child);
                 _children = newChildren;
             }
@@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.Entities
         {
             lock (_childrenSyncLock)
             {
-                _children = _children.Except(children).ToList();
+                _children = ActualChildren.Except(children).ToList();
             }
         }
 
@@ -519,7 +519,7 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// The children
         /// </summary>
-        private IReadOnlyList<BaseItem> _children = new List<BaseItem>();
+        private IReadOnlyList<BaseItem> _children;
         /// <summary>
         /// The _children sync lock
         /// </summary>
@@ -532,15 +532,10 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                return _children;
+                return _children ?? (_children = LoadChildrenInternal());
             }
         }
 
-        public void LoadSavedChildren()
-        {
-            _children = LoadChildrenInternal();
-        }
-
         /// <summary>
         /// thread-safe access to the actual children of this folder - without regard to user
         /// </summary>
@@ -758,7 +753,7 @@ namespace MediaBrowser.Controller.Entities
 
                 AddChildrenInternal(newItems);
 
-                await ItemRepository.SaveChildren(Id, _children.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
+                await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
 
                 //force the indexes to rebuild next time
                 if (IndexCache != null)
@@ -1007,8 +1002,7 @@ namespace MediaBrowser.Controller.Entities
                 return result;
             }
 
-            var initialCount = _children.Count;
-            var list = new List<BaseItem>(initialCount);
+            var list = new List<BaseItem>();
 
             AddChildrenToList(user, includeLinkedChildren, list, false, null);
 
@@ -1038,16 +1032,13 @@ namespace MediaBrowser.Controller.Entities
                     }
                 }
 
-                if (recursive)
+                if (recursive && child.IsFolder)
                 {
-                    var folder = child as Folder;
+                    var folder = (Folder)child;
 
-                    if (folder != null)
+                    if (folder.AddChildrenToList(user, includeLinkedChildren, list, true, filter))
                     {
-                        if (folder.AddChildrenToList(user, includeLinkedChildren, list, true, filter))
-                        {
-                            hasLinkedChildren = true;
-                        }
+                        hasLinkedChildren = true;
                     }
                 }
             }
@@ -1073,7 +1064,6 @@ namespace MediaBrowser.Controller.Entities
             return hasLinkedChildren;
         }
 
-        private int _lastRecursiveCount;
         /// <summary>
         /// Gets allowed recursive children of an item
         /// </summary>
@@ -1101,13 +1091,10 @@ namespace MediaBrowser.Controller.Entities
                 throw new ArgumentNullException("user");
             }
 
-            var initialCount = _lastRecursiveCount == 0 ? _children.Count : _lastRecursiveCount;
-            var list = new List<BaseItem>(initialCount);
+            var list = new List<BaseItem>();
 
             var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, true, filter);
 
-            _lastRecursiveCount = list.Count;
-
             return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
         }
 
@@ -1127,8 +1114,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>IEnumerable{BaseItem}.</returns>
         public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
         {
-            var initialCount = _lastRecursiveCount == 0 ? _children.Count : _lastRecursiveCount;
-            var list = new List<BaseItem>(initialCount);
+            var list = new List<BaseItem>();
 
             AddChildrenToList(list, true, filter);
 
@@ -1150,14 +1136,11 @@ namespace MediaBrowser.Controller.Entities
                     list.Add(child);
                 }
 
-                if (recursive)
+                if (recursive && child.IsFolder)
                 {
-                    var folder = child as Folder;
+                    var folder = (Folder)child;
 
-                    if (folder != null)
-                    {
-                        folder.AddChildrenToList(list, true, filter);
-                    }
+                    folder.AddChildrenToList(list, true, filter);
                 }
             }
         }

+ 22 - 2
MediaBrowser.Controller/Entities/Game.cs

@@ -4,16 +4,26 @@ using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class Game : BaseItem, IHasSoundtracks
+    public class Game : BaseItem, IHasSoundtracks, IHasTrailers
     {
         public List<Guid> SoundtrackIds { get; set; }
-        
+
         public Game()
         {
             MultiPartGameFiles = new List<string>();
             SoundtrackIds = new List<Guid>();
+            RemoteTrailers = new List<MediaUrl>();
+            LocalTrailerIds = new List<Guid>();
         }
 
+        public List<Guid> LocalTrailerIds { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the remote trailers.
+        /// </summary>
+        /// <value>The remote trailers.</value>
+        public List<MediaUrl> RemoteTrailers { get; set; }
+        
         /// <summary>
         /// Gets the type of the media.
         /// </summary>
@@ -84,5 +94,15 @@ namespace MediaBrowser.Controller.Entities
             }
             return base.GetUserDataKey();
         }
+
+        public override IEnumerable<string> GetDeletePaths()
+        {
+            if (!IsInMixedFolder)
+            {
+                return new[] { System.IO.Path.GetDirectoryName(Path) };
+            }
+
+            return base.GetDeletePaths();
+        }
     }
 }

+ 2 - 2
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities
     {
         public GameGenre()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         /// <summary>
@@ -22,6 +22,6 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
     }
 }

+ 4 - 4
MediaBrowser.Controller/Entities/Genre.cs

@@ -1,7 +1,7 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Dto;
 using System;
 using System.Collections.Generic;
+using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
     {
         public Genre()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         /// <summary>
@@ -25,6 +25,6 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
     }
 }

+ 14 - 0
MediaBrowser.Controller/Entities/IHasAspectRatio.cs

@@ -0,0 +1,14 @@
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Interface IHasAspectRatio
+    /// </summary>
+    public interface IHasAspectRatio
+    {
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        string AspectRatio { get; set; }
+    }
+}

+ 18 - 0
MediaBrowser.Controller/Entities/IHasBudget.cs

@@ -0,0 +1,18 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasBudget
+    {
+        /// <summary>
+        /// Gets or sets the budget.
+        /// </summary>
+        /// <value>The budget.</value>
+        double? Budget { get; set; }
+
+        /// <summary>
+        /// Gets or sets the revenue.
+        /// </summary>
+        /// <value>The revenue.</value>
+        double? Revenue { get; set; }
+    }
+}

+ 21 - 0
MediaBrowser.Controller/Entities/IHasTrailers.cs

@@ -0,0 +1,21 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public interface IHasTrailers
+    {
+        /// <summary>
+        /// Gets or sets the remote trailers.
+        /// </summary>
+        /// <value>The remote trailers.</value>
+        List<MediaUrl> RemoteTrailers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the local trailer ids.
+        /// </summary>
+        /// <value>The local trailer ids.</value>
+        List<Guid> LocalTrailerIds { get; set; }
+    }
+}

+ 21 - 9
MediaBrowser.Controller/Entities/IItemByName.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Model.Dto;
 using System;
 using System.Collections.Generic;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -9,26 +10,37 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public interface IItemByName
     {
-        Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        List<ItemByNameCounts> UserItemCountList { get; set; }
     }
 
-    public static class IItemByNameExtensions
+    public interface IHasDualAccess : IItemByName
     {
-        public static ItemByNameCounts GetItemByNameCounts(this IItemByName item, User user)
+        bool IsAccessedByName { get; }
+    }
+
+    public static class ItemByNameExtensions
+    {
+        public static ItemByNameCounts GetItemByNameCounts(this IItemByName item, Guid userId)
         {
-            if (user == null)
+            if (userId == Guid.Empty)
             {
-                throw new ArgumentNullException("user");
+                throw new ArgumentNullException("userId");
             }
 
-            ItemByNameCounts counts;
+            return item.UserItemCountList.FirstOrDefault(i => i.UserId == userId);
+        }
+
+        public static void SetItemByNameCounts(this IItemByName item, Guid userId, ItemByNameCounts counts)
+        {
+            var current = item.UserItemCountList.FirstOrDefault(i => i.UserId == userId);
 
-            if (item.UserItemCounts.TryGetValue(user.Id, out counts))
+            if (current != null)
             {
-                return counts;
+                item.UserItemCountList.Remove(current);
             }
 
-            return null;
+            counts.UserId = userId;
+            item.UserItemCountList.Add(counts);
         }
     }
 }

+ 0 - 1
MediaBrowser.Controller/Entities/IndexFolder.cs

@@ -40,7 +40,6 @@ namespace MediaBrowser.Controller.Entities
 
             IndexName = indexName;
             Parent = parent;
-            LoadSavedChildren();
         }
 
         /// <summary>

+ 18 - 2
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -1,10 +1,26 @@
-
+using System;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities.Movies
 {
     /// <summary>
     /// Class BoxSet
     /// </summary>
-    public class BoxSet : Folder
+    public class BoxSet : Folder, IHasTrailers
     {
+        public BoxSet()
+        {
+            RemoteTrailers = new List<MediaUrl>();
+            LocalTrailerIds = new List<Guid>();
+        }
+
+        public List<Guid> LocalTrailerIds { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the remote trailers.
+        /// </summary>
+        /// <value>The remote trailers.</value>
+        public List<MediaUrl> RemoteTrailers { get; set; }
     }
 }

+ 19 - 1
MediaBrowser.Controller/Entities/Movies/Movie.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Entities.Movies
     /// <summary>
     /// Class Movie
     /// </summary>
-    public class Movie : Video, IHasCriticRating, IHasSoundtracks
+    public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers
     {
         public List<Guid> SpecialFeatureIds { get; set; }
 
@@ -21,8 +21,26 @@ namespace MediaBrowser.Controller.Entities.Movies
         {
             SpecialFeatureIds = new List<Guid>();
             SoundtrackIds = new List<Guid>();
+            RemoteTrailers = new List<MediaUrl>();
+            LocalTrailerIds = new List<Guid>();
         }
 
+        public List<Guid> LocalTrailerIds { get; set; }
+        
+        public List<MediaUrl> RemoteTrailers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the budget.
+        /// </summary>
+        /// <value>The budget.</value>
+        public double? Budget { get; set; }
+
+        /// <summary>
+        /// Gets or sets the revenue.
+        /// </summary>
+        /// <value>The revenue.</value>
+        public double? Revenue { get; set; }
+
         /// <summary>
         /// Gets or sets the critic rating.
         /// </summary>

+ 13 - 1
MediaBrowser.Controller/Entities/MusicVideo.cs

@@ -4,7 +4,7 @@ using System;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class MusicVideo : Video, IHasArtist, IHasMusicGenres
+    public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasBudget
     {
         /// <summary>
         /// Gets or sets the artist.
@@ -18,6 +18,18 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The album.</value>
         public string Album { get; set; }
 
+        /// <summary>
+        /// Gets or sets the budget.
+        /// </summary>
+        /// <value>The budget.</value>
+        public double? Budget { get; set; }
+
+        /// <summary>
+        /// Gets or sets the revenue.
+        /// </summary>
+        /// <value>The revenue.</value>
+        public double? Revenue { get; set; }
+
         /// <summary>
         /// Determines whether the specified name has artist.
         /// </summary>

+ 10 - 4
MediaBrowser.Controller/Entities/Person.cs

@@ -1,7 +1,7 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Dto;
 using System;
 using System.Collections.Generic;
+using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -12,11 +12,11 @@ namespace MediaBrowser.Controller.Entities
     {
         public Person()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
         
         /// <summary>
         /// Gets the user data key.
@@ -49,6 +49,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The type.</value>
         public string Type { get; set; }
 
+        /// <summary>
+        /// Gets or sets the sort order - ascending
+        /// </summary>
+        /// <value>The sort order.</value>
+        public int? SortOrder { get; set; }
+
         /// <summary>
         /// Returns a <see cref="System.String" /> that represents this instance.
         /// </summary>

+ 2 - 2
MediaBrowser.Controller/Entities/Studio.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
     {
         public Studio()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         /// <summary>
@@ -25,6 +25,6 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
     }
 }

+ 69 - 15
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -50,6 +50,33 @@ namespace MediaBrowser.Controller.Entities.TV
             get { return true; }
         }
 
+        [IgnoreDataMember]
+        public int? AiredSeasonNumber
+        {
+            get
+            {
+                return AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? PhysicalSeasonNumber;
+            }
+        }
+
+        [IgnoreDataMember]
+        public int? PhysicalSeasonNumber
+        {
+            get
+            {
+                var value = ParentIndexNumber;
+
+                if (value.HasValue)
+                {
+                    return value;
+                }
+
+                var season = Parent as Season;
+
+                return season != null ? season.IndexNumber : null;
+            }
+        }
+
         /// <summary>
         /// We roll up into series
         /// </summary>
@@ -59,7 +86,7 @@ namespace MediaBrowser.Controller.Entities.TV
         {
             get
             {
-                return Season;
+                return FindParent<Season>();
             }
         }
 
@@ -151,20 +178,6 @@ namespace MediaBrowser.Controller.Entities.TV
             get { return _series ?? (_series = FindParent<Series>()); }
         }
 
-        /// <summary>
-        /// The _season
-        /// </summary>
-        private Season _season;
-        /// <summary>
-        /// This Episode's Season Instance
-        /// </summary>
-        /// <value>The season.</value>
-        [IgnoreDataMember]
-        public Season Season
-        {
-            get { return _season ?? (_season = FindParent<Season>()); }
-        }
-
         /// <summary>
         /// This is the ending episode number for double episodes.
         /// </summary>
@@ -221,5 +234,46 @@ namespace MediaBrowser.Controller.Entities.TV
         {
             get { return LocationType == Model.Entities.LocationType.Virtual && IsUnaired; }
         }
+
+        [IgnoreDataMember]
+        public Guid? SeasonId
+        {
+            get
+            {
+                // First see if the parent is a Season
+                var season = Parent as Season;
+
+                if (season != null)
+                {
+                    return season.Id;
+                }
+
+                var seasonNumber = ParentIndexNumber;
+
+                // Parent is a Series
+                if (seasonNumber.HasValue)
+                {
+                    var series = Parent as Series;
+
+                    if (series != null)
+                    {
+                        season = series.Children.OfType<Season>()
+                            .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber.Value);
+
+                        if (season != null)
+                        {
+                            return season.Id;
+                        }
+                    }
+                }
+
+                return null;
+            }
+        }
+
+        public override IEnumerable<string> GetDeletePaths()
+        {
+            return new[] { Path };
+        }
     }
 }

+ 45 - 5
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -1,9 +1,9 @@
-using System.Linq;
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities.TV
@@ -94,6 +94,22 @@ namespace MediaBrowser.Controller.Entities.TV
             get { return _series ?? (_series = FindParent<Series>()); }
         }
 
+        [IgnoreDataMember]
+        public string SeriesPath
+        {
+            get
+            {
+                var series = Series;
+
+                if (series != null)
+                {
+                    return series.Path;
+                }
+
+                return System.IO.Path.GetDirectoryName(Path);
+            }
+        }
+
         /// <summary>
         /// Our rating comes from our series
         /// </summary>
@@ -149,16 +165,34 @@ namespace MediaBrowser.Controller.Entities.TV
             return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
         }
 
+        private IEnumerable<Episode> GetEpisodes()
+        {
+            var series = Series;
+
+            if (series != null && series.ContainsEpisodesWithoutSeasonFolders)
+            {
+                var seasonNumber = IndexNumber;
+
+                if (seasonNumber.HasValue)
+                {
+                    return series.RecursiveChildren.OfType<Episode>()
+                        .Where(i => i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == seasonNumber.Value);
+                }
+            }
+
+            return Children.OfType<Episode>();
+        }
+
         [IgnoreDataMember]
         public bool IsMissingSeason
         {
-            get { return LocationType == Model.Entities.LocationType.Virtual && Children.OfType<Episode>().All(i => i.IsMissingEpisode); }
+            get { return LocationType == Model.Entities.LocationType.Virtual && GetEpisodes().All(i => i.IsMissingEpisode); }
         }
 
         [IgnoreDataMember]
         public bool IsUnaired
         {
-            get { return Children.OfType<Episode>().All(i => i.IsUnaired); }
+            get { return GetEpisodes().All(i => i.IsUnaired); }
         }
 
         [IgnoreDataMember]
@@ -170,7 +204,13 @@ namespace MediaBrowser.Controller.Entities.TV
         [IgnoreDataMember]
         public bool IsMissingOrVirtualUnaired
         {
-            get { return LocationType == Model.Entities.LocationType.Virtual && Children.OfType<Episode>().All(i => i.IsVirtualUnaired || i.IsMissingEpisode); }
+            get { return LocationType == Model.Entities.LocationType.Virtual && GetEpisodes().All(i => i.IsVirtualUnaired || i.IsMissingEpisode); }
+        }
+
+        [IgnoreDataMember]
+        public bool IsSpecialSeason
+        {
+            get { return (IndexNumber ?? -1) == 0; }
         }
     }
 }

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

@@ -4,6 +4,7 @@ using MediaBrowser.Model.Entities;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities.TV
@@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities.TV
     /// <summary>
     /// Class Series
     /// </summary>
-    public class Series : Folder, IHasSoundtracks
+    public class Series : Folder, IHasSoundtracks, IHasTrailers
     {
         public List<Guid> SpecialFeatureIds { get; set; }
         public List<Guid> SoundtrackIds { get; set; }
@@ -24,8 +25,14 @@ namespace MediaBrowser.Controller.Entities.TV
 
             SpecialFeatureIds = new List<Guid>();
             SoundtrackIds = new List<Guid>();
+            RemoteTrailers = new List<MediaUrl>();
+            LocalTrailerIds = new List<Guid>();
         }
 
+        public List<Guid> LocalTrailerIds { get; set; }
+        
+        public List<MediaUrl> RemoteTrailers { get; set; }
+        
         /// <summary>
         /// Gets or sets the status.
         /// </summary>
@@ -94,5 +101,14 @@ namespace MediaBrowser.Controller.Entities.TV
 
             return args;
         }
+
+        [IgnoreDataMember]
+        public bool ContainsEpisodesWithoutSeasonFolders
+        {
+            get
+            {
+                return Children.OfType<Video>().Any();
+            }
+        }
     }
 }

+ 18 - 1
MediaBrowser.Controller/Entities/Trailer.cs

@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// Class Trailer
     /// </summary>
-    public class Trailer : Video, IHasCriticRating, IHasSoundtracks
+    public class Trailer : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers
     {
         public List<Guid> SoundtrackIds { get; set; }
         
@@ -17,8 +17,25 @@ namespace MediaBrowser.Controller.Entities
             RemoteTrailers = new List<MediaUrl>();
             Taglines = new List<string>();
             SoundtrackIds = new List<Guid>();
+            LocalTrailerIds = new List<Guid>();
         }
 
+        public List<Guid> LocalTrailerIds { get; set; }
+        
+        public List<MediaUrl> RemoteTrailers { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the budget.
+        /// </summary>
+        /// <value>The budget.</value>
+        public double? Budget { get; set; }
+
+        /// <summary>
+        /// Gets or sets the revenue.
+        /// </summary>
+        /// <value>The revenue.</value>
+        public double? Revenue { get; set; }
+
         /// <summary>
         /// Gets or sets the critic rating.
         /// </summary>

+ 19 - 1
MediaBrowser.Controller/Entities/Video.cs

@@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// Class Video
     /// </summary>
-    public class Video : BaseItem, IHasMediaStreams
+    public class Video : BaseItem, IHasMediaStreams, IHasAspectRatio
     {
         public bool IsMultiPart { get; set; }
 
@@ -65,6 +65,12 @@ namespace MediaBrowser.Controller.Entities
             return GetPlayableStreamFiles(Path);
         }
 
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        public string AspectRatio { get; set; }
+        
         /// <summary>
         /// Should be overridden to return the proper folder where metadata lives
         /// </summary>
@@ -252,5 +258,17 @@ namespace MediaBrowser.Controller.Entities
             }).ToList();
         }
 
+        public override IEnumerable<string> GetDeletePaths()
+        {
+            if (!IsInMixedFolder)
+            {
+                if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso)
+                {
+                    return new[] { System.IO.Path.GetDirectoryName(Path) };
+                }
+            }
+
+            return base.GetDeletePaths();
+        }
     }
 }

+ 2 - 2
MediaBrowser.Controller/Entities/Year.cs

@@ -12,11 +12,11 @@ namespace MediaBrowser.Controller.Entities
     {
         public Year()
         {
-            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+            UserItemCountList = new List<ItemByNameCounts>();
         }
 
         [IgnoreDataMember]
-        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        public List<ItemByNameCounts> UserItemCountList { get; set; }
 
         /// <summary>
         /// Gets the user data key.

+ 14 - 1
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>Task{Artist}.</returns>
-        Artist GetArtist(string name);
+        MusicArtist GetArtist(string name);
 
         /// <summary>
         /// Gets a Studio
@@ -302,5 +302,18 @@ namespace MediaBrowser.Controller.Library
         /// <param name="updateType">Type of the update.</param>
         /// <returns>Task.</returns>
         Task SaveMetadata(BaseItem item, ItemUpdateType updateType);
+
+        /// <summary>
+        /// Gets all artists.
+        /// </summary>
+        /// <returns>IEnumerable{System.String}.</returns>
+        IEnumerable<string> GetAllArtists();
+
+        /// <summary>
+        /// Gets all artists.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <returns>IEnumerable{System.String}.</returns>
+        IEnumerable<string> GetAllArtists(IEnumerable<BaseItem> items);
     }
 }

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

@@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Library
 
                 if (match != null)
                 {
-                    return ParseEpisodeNumber(match.Value);
+                    return ParseEpisodeNumber(match.Groups["epnumber"].Value);
                 }
             }
 

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

@@ -0,0 +1,73 @@
+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; }
+
+        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;
+            }
+        }
+    }
+}

+ 0 - 6
MediaBrowser.Controller/LiveTv/ChannelInfo.cs

@@ -25,12 +25,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The id of the channel.</value>
         public string Id { 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>

+ 84 - 6
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -1,6 +1,8 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
 using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -15,6 +17,27 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The services.</value>
         IReadOnlyList<ILiveTvService> Services { get; }
 
+        /// <summary>
+        /// Schedules the recording.
+        /// </summary>
+        /// <param name="programId">The program identifier.</param>
+        /// <returns>Task.</returns>
+        Task ScheduleRecording(string programId);
+
+        /// <summary>
+        /// Deletes the recording.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>Task.</returns>
+        Task DeleteRecording(string id);
+
+        /// <summary>
+        /// Cancels the timer.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>Task.</returns>
+        Task CancelTimer(string id);
+        
         /// <summary>
         /// Adds the parts.
         /// </summary>
@@ -22,10 +45,65 @@ namespace MediaBrowser.Controller.LiveTv
         void AddParts(IEnumerable<ILiveTvService> services);
 
         /// <summary>
-        /// Gets the channel info dto.
+        /// Gets the channels.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>IEnumerable{Channel}.</returns>
+        QueryResult<ChannelInfoDto> GetChannels(ChannelQuery query);
+
+        /// <summary>
+        /// Gets the recording.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{RecordingInfoDto}.</returns>
+        Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the timer.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{TimerInfoDto}.</returns>
+        Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the recordings.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>QueryResult{RecordingInfoDto}.</returns>
+        Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the timers.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{QueryResult{TimerInfoDto}}.</returns>
+        Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken);
+        
+        /// <summary>
+        /// Gets the channel.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>Channel.</returns>
+        Channel GetChannel(string id);
+
+        /// <summary>
+        /// Gets the channel.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="userId">The user identifier.</param>
+        /// <returns>Channel.</returns>
+        ChannelInfoDto GetChannelInfoDto(string id, string userId);
+
+        /// <summary>
+        /// Gets the programs.
         /// </summary>
-        /// <param name="info">The info.</param>
-        /// <returns>ChannelInfoDto.</returns>
-        ChannelInfoDto GetChannelInfoDto(ChannelInfo info);
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>IEnumerable{ProgramInfo}.</returns>
+        Task<QueryResult<ProgramInfoDto>> GetPrograms(ProgramQuery query, CancellationToken cancellationToken);
     }
 }

+ 76 - 7
MediaBrowser.Controller/LiveTv/ILiveTvService.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Common.Net;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
@@ -23,20 +23,89 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task{IEnumerable{ChannelInfo}}.</returns>
         Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken);
 
+        /// <summary>
+        /// Cancels the timer asynchronous.
+        /// </summary>
+        /// <param name="timerId">The timer identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CancelTimerAsync(string timerId, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Deletes the recording asynchronous.
+        /// </summary>
+        /// <param name="recordingId">The recording identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Creates the timer asynchronous.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Creates the series timer asynchronous.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Updates the series timer asynchronous.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
+        
+        /// <summary>
+        /// Gets the channel image asynchronous.
+        /// </summary>
+        /// <param name="channelId">The channel identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{Stream}.</returns>
+        Task<ImageResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the program image asynchronous.
+        /// </summary>
+        /// <param name="programId">The program identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{ImageResponseInfo}.</returns>
+        Task<ImageResponseInfo> GetProgramImageAsync(string programId, CancellationToken cancellationToken);
+        
         /// <summary>
         /// Gets the recordings asynchronous.
         /// </summary>
-        /// <param name="query">The query.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{RecordingInfo}}.</returns>
-        Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(RecordingQuery query, CancellationToken cancellationToken);
+        Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken);
 
         /// <summary>
-        /// Gets the channel guides.
+        /// Gets the recordings asynchronous.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RecordingInfo}}.</returns>
+        Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the series timers asynchronous.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{SeriesTimerInfo}}.</returns>
+        Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken);
+        
+        /// <summary>
+        /// Gets the programs asynchronous.
         /// </summary>
-        /// <param name="channelIdList">The channel identifier list.</param>
+        /// <param name="channelId">The channel identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{ChannelGuide}}.</returns>
-        Task<IEnumerable<ChannelGuide>> GetChannelGuidesAsync(IEnumerable<string> channelIdList, CancellationToken cancellationToken);
+        /// <returns>Task{IEnumerable{ProgramInfo}}.</returns>
+        Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, CancellationToken cancellationToken);
     }
 }

+ 19 - 0
MediaBrowser.Controller/LiveTv/ImageResponseInfo.cs

@@ -0,0 +1,19 @@
+using System.IO;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class ImageResponseInfo
+    {
+        /// <summary>
+        /// Gets or sets the stream.
+        /// </summary>
+        /// <value>The stream.</value>
+        public Stream Stream { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the MIME.
+        /// </summary>
+        /// <value>The type of the MIME.</value>
+        public string MimeType { get; set; }
+    }
+}

+ 98 - 0
MediaBrowser.Controller/LiveTv/ProgramInfo.cs

@@ -0,0 +1,98 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class ProgramInfo
+    {
+        /// <summary>
+        /// Id of the program.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the channel identifier.
+        /// </summary>
+        /// <value>The channel identifier.</value>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// Name of the program
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the official rating.
+        /// </summary>
+        /// <value>The official rating.</value>
+        public string OfficialRating { get; set; }
+        
+        /// <summary>
+        /// Description of the progam.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the program, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the program, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        public string AspectRatio { get; set; }
+
+        /// <summary>
+        /// Genre of the program.
+        /// </summary>
+        public List<string> Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets the quality.
+        /// </summary>
+        /// <value>The quality.</value>
+        public ProgramVideoQuality Quality { get; set; }
+
+        /// <summary>
+        /// Gets or sets the original air date.
+        /// </summary>
+        /// <value>The original air date.</value>
+        public DateTime? OriginalAirDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio.
+        /// </summary>
+        /// <value>The audio.</value>
+        public ProgramAudio Audio { get; set; }
+
+        /// <summary>
+        /// Gets or sets the community rating.
+        /// </summary>
+        /// <value>The community rating.</value>
+        public float? CommunityRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is repeat.
+        /// </summary>
+        /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+        public bool IsRepeat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode title.
+        /// </summary>
+        /// <value>The episode title.</value>
+        public string EpisodeTitle { get; set; }
+        
+        public ProgramInfo()
+        {
+            Genres = new List<string>();
+        }
+    }
+}

+ 102 - 0
MediaBrowser.Controller/LiveTv/RecordingInfo.cs

@@ -0,0 +1,102 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class RecordingInfo
+    {
+        /// <summary>
+        /// Id of the recording.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// ChannelId of the recording.
+        /// </summary>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// ChannelName of the recording.
+        /// </summary>
+        public string ChannelName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the channel.
+        /// </summary>
+        /// <value>The type of the channel.</value>
+        public ChannelType ChannelType { get; set; }
+     
+        /// <summary>
+        /// Name of the recording.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+        
+        /// <summary>
+        /// Description of the recording.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the recording, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the recording, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the program identifier.
+        /// </summary>
+        /// <value>The program identifier.</value>
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the status.
+        /// </summary>
+        /// <value>The status.</value>
+        public RecordingStatus Status { get; set; }
+
+        /// <summary>
+        /// Genre of the program.
+        /// </summary>
+        public List<string> Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is repeat.
+        /// </summary>
+        /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+        public bool IsRepeat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode title.
+        /// </summary>
+        /// <value>The episode title.</value>
+        public string EpisodeTitle { get; set; }
+
+        /// <summary>
+        /// Gets or sets the official rating.
+        /// </summary>
+        /// <value>The official rating.</value>
+        public string OfficialRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the community rating.
+        /// </summary>
+        /// <value>The community rating.</value>
+        public float? CommunityRating { get; set; }
+
+        public RecordingInfo()
+        {
+            Genres = new List<string>();
+        }
+    }
+}

+ 85 - 0
MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs

@@ -0,0 +1,85 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class SeriesTimerInfo
+    {
+        /// <summary>
+        /// Id of the recording.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// ChannelId of the recording.
+        /// </summary>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// ChannelName of the recording.
+        /// </summary>
+        public string ChannelName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the program identifier.
+        /// </summary>
+        /// <value>The program identifier.</value>
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Name of the recording.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Description of the recording.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the recording, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the recording, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the pre padding seconds.
+        /// </summary>
+        /// <value>The pre padding seconds.</value>
+        public int PrePaddingSeconds { get; set; }
+
+        /// <summary>
+        /// Gets or sets the post padding seconds.
+        /// </summary>
+        /// <value>The post padding seconds.</value>
+        public int PostPaddingSeconds { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the recurrence.
+        /// </summary>
+        /// <value>The type of the recurrence.</value>
+        public RecurrenceType RecurrenceType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the days.
+        /// </summary>
+        /// <value>The days.</value>
+        public List<DayOfWeek> Days { get; set; }
+
+        /// <summary>
+        /// Gets or sets the priority.
+        /// </summary>
+        /// <value>The priority.</value>
+        public int Priority { get; set; }
+
+        public SeriesTimerInfo()
+        {
+            Days = new List<DayOfWeek>();
+        }
+    }
+}

+ 73 - 0
MediaBrowser.Controller/LiveTv/TimerInfo.cs

@@ -0,0 +1,73 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class TimerInfo
+    {
+        /// <summary>
+        /// Id of the recording.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the series timer identifier.
+        /// </summary>
+        /// <value>The series timer identifier.</value>
+        public string SeriesTimerId { get; set; }
+        
+        /// <summary>
+        /// ChannelId of the recording.
+        /// </summary>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// ChannelName of the recording.
+        /// </summary>
+        public string ChannelName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the program identifier.
+        /// </summary>
+        /// <value>The program identifier.</value>
+        public string ProgramId { get; set; }
+        
+        /// <summary>
+        /// Name of the recording.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Description of the recording.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the recording, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the recording, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the status.
+        /// </summary>
+        /// <value>The status.</value>
+        public RecordingStatus Status { get; set; }
+
+        /// <summary>
+        /// Gets or sets the pre padding seconds.
+        /// </summary>
+        /// <value>The pre padding seconds.</value>
+        public int PrePaddingSeconds { get; set; }
+
+        /// <summary>
+        /// Gets or sets the post padding seconds.
+        /// </summary>
+        /// <value>The post padding seconds.</value>
+        public int PostPaddingSeconds { get; set; }
+    }
+}

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

@@ -89,8 +89,11 @@
     <Compile Include="Entities\GameGenre.cs" />
     <Compile Include="Entities\GameSystem.cs" />
     <Compile Include="Entities\IByReferenceItem.cs" />
+    <Compile Include="Entities\IHasAspectRatio.cs" />
+    <Compile Include="Entities\IHasBudget.cs" />
     <Compile Include="Entities\IHasCriticRating.cs" />
     <Compile Include="Entities\IHasSoundtracks.cs" />
+    <Compile Include="Entities\IHasTrailers.cs" />
     <Compile Include="Entities\IItemByName.cs" />
     <Compile Include="Entities\ILibraryItem.cs" />
     <Compile Include="Entities\ImageSourceInfo.cs" />
@@ -103,18 +106,24 @@
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
+    <Compile Include="LiveTv\Channel.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
     <Compile Include="LiveTv\ILiveTvService.cs" />
+    <Compile Include="LiveTv\ImageResponseInfo.cs" />
+    <Compile Include="LiveTv\ProgramInfo.cs" />
+    <Compile Include="LiveTv\RecordingInfo.cs" />
+    <Compile Include="LiveTv\SeriesTimerInfo.cs" />
+    <Compile Include="LiveTv\TimerInfo.cs" />
     <Compile Include="Localization\ILocalizationManager.cs" />
     <Compile Include="Notifications\INotificationsRepository.cs" />
     <Compile Include="Notifications\NotificationUpdateEventArgs.cs" />
     <Compile Include="Providers\IDynamicInfoProvider.cs" />
     <Compile Include="Providers\IImageProvider.cs" />
+    <Compile Include="Providers\NameParser.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Entities\AggregateFolder.cs" />
-    <Compile Include="Entities\Audio\Artist.cs" />
     <Compile Include="Entities\Audio\Audio.cs" />
     <Compile Include="Entities\Audio\MusicAlbum.cs" />
     <Compile Include="Entities\Audio\MusicArtist.cs" />

+ 97 - 13
MediaBrowser.Controller/Providers/BaseItemXmlParser.cs

@@ -69,6 +69,12 @@ namespace MediaBrowser.Controller.Providers
             item.People.Clear();
             item.Tags.Clear();
 
+            var hasTrailers = item as IHasTrailers;
+            if (hasTrailers != null)
+            {
+                hasTrailers.RemoteTrailers.Clear();
+            }
+
             //Fetch(item, metadataFile, settings, Encoding.GetEncoding("ISO-8859-1"), cancellationToken);
             Fetch(item, metadataFile, settings, Encoding.UTF8, cancellationToken);
         }
@@ -161,10 +167,14 @@ namespace MediaBrowser.Controller.Providers
                 case "Budget":
                     {
                         var text = reader.ReadElementContentAsString();
-                        double value;
-                        if (double.TryParse(text, NumberStyles.Any, _usCulture, out value))
+                        var hasBudget = item as IHasBudget;
+                        if (hasBudget != null)
                         {
-                            item.Budget = value;
+                            double value;
+                            if (double.TryParse(text, NumberStyles.Any, _usCulture, out value))
+                            {
+                                hasBudget.Budget = value;
+                            }
                         }
 
                         break;
@@ -173,10 +183,14 @@ namespace MediaBrowser.Controller.Providers
                 case "Revenue":
                     {
                         var text = reader.ReadElementContentAsString();
-                        double value;
-                        if (double.TryParse(text, NumberStyles.Any, _usCulture, out value))
+                        var hasBudget = item as IHasBudget;
+                        if (hasBudget != null)
                         {
-                            item.Revenue = value;
+                            double value;
+                            if (double.TryParse(text, NumberStyles.Any, _usCulture, out value))
+                            {
+                                hasBudget.Revenue = value;
+                            }
                         }
 
                         break;
@@ -375,9 +389,10 @@ namespace MediaBrowser.Controller.Providers
                     {
                         var val = reader.ReadElementContentAsString();
 
-                        if (!string.IsNullOrWhiteSpace(val))
+                        var hasAspectRatio = item as IHasAspectRatio;
+                        if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
                         {
-                            item.AspectRatio = val;
+                            hasAspectRatio.AspectRatio = val;
                         }
                         break;
                     }
@@ -474,9 +489,26 @@ namespace MediaBrowser.Controller.Providers
                     {
                         var val = reader.ReadElementContentAsString();
 
-                        if (!string.IsNullOrWhiteSpace(val))
+                        var hasTrailers = item as IHasTrailers;
+                        if (hasTrailers != null)
                         {
-                            item.AddTrailerUrl(val, false);
+                            if (!string.IsNullOrWhiteSpace(val))
+                            {
+                                hasTrailers.AddTrailerUrl(val, false);
+                            }
+                        }
+                        break;
+                    }
+
+                case "Trailers":
+                    {
+                        using (var subtree = reader.ReadSubtree())
+                        {
+                            var hasTrailers = item as IHasTrailers;
+                            if (hasTrailers != null)
+                            {
+                                FetchDataFromTrailersNode(subtree, hasTrailers);
+                            }
                         }
                         break;
                     }
@@ -921,6 +953,35 @@ namespace MediaBrowser.Controller.Providers
             }
         }
 
+        private void FetchDataFromTrailersNode(XmlReader reader, IHasTrailers item)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "Trailer":
+                            {
+                                var val = reader.ReadElementContentAsString();
+
+                                if (!string.IsNullOrWhiteSpace(val))
+                                {
+                                    item.AddTrailerUrl(val, false);
+                                }
+                                break;
+                            }
+
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+        }
+        
         protected async Task FetchChaptersFromXmlNode(BaseItem item, XmlReader reader, IItemRepository repository, CancellationToken cancellationToken)
         {
             var runtime = item.RunTimeTicks ?? 0;
@@ -1071,9 +1132,10 @@ namespace MediaBrowser.Controller.Providers
         /// <returns>IEnumerable{PersonInfo}.</returns>
         private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
         {
-            var names = new List<string>();
+            var name = string.Empty;
             var type = "Actor";  // If type is not specified assume actor
             var role = string.Empty;
+            int? sortOrder = null;
 
             reader.MoveToContent();
 
@@ -1084,7 +1146,7 @@ namespace MediaBrowser.Controller.Providers
                     switch (reader.Name)
                     {
                         case "Name":
-                            names.AddRange(SplitNames(reader.ReadElementContentAsString()));
+                            name = reader.ReadElementContentAsString() ?? string.Empty;
                             break;
 
                         case "Type":
@@ -1108,6 +1170,20 @@ namespace MediaBrowser.Controller.Providers
                                 }
                                 break;
                             }
+                        case "SortOrder":
+                            {
+                                var val = reader.ReadElementContentAsString();
+
+                                if (!string.IsNullOrWhiteSpace(val))
+                                {
+                                    int intVal;
+                                    if (int.TryParse(val, NumberStyles.Integer, _usCulture, out intVal))
+                                    {
+                                        sortOrder = intVal;
+                                    }
+                                }
+                                break;
+                            }
 
                         default:
                             reader.Skip();
@@ -1116,7 +1192,15 @@ namespace MediaBrowser.Controller.Providers
                 }
             }
 
-            return names.Select(n => new PersonInfo { Name = n.Trim(), Role = role, Type = type });
+            var personInfo = new PersonInfo
+            {
+                Name = name.Trim(), 
+                Role = role, 
+                Type = type, 
+                SortOrder = sortOrder
+            };
+
+            return new[] { personInfo };
         }
 
         /// <summary>

+ 39 - 0
MediaBrowser.Controller/Providers/NameParser.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public static class NameParser
+    {
+        static readonly Regex[] NameMatches = new[] {
+            new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year
+            new Regex(@"(?<name>.*)(\.(?<year>\d{4})(\.|$)).*$"), 
+            new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
+        };
+
+
+        /// <summary>
+        /// Parses the name.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="justName">Name of the just.</param>
+        /// <param name="year">The year.</param>
+        public static void ParseName(string name, out string justName, out int? year)
+        {
+            justName = null;
+            year = null;
+            foreach (var re in NameMatches)
+            {
+                Match m = re.Match(name);
+                if (m.Success)
+                {
+                    justName = m.Groups["name"].Value.Trim();
+                    string y = m.Groups["year"] != null ? m.Groups["year"].Value : null;
+                    int temp;
+                    year = Int32.TryParse(y, out temp) ? temp : (int?)null;
+                    break;
+                }
+            }
+        }
+    }
+}

+ 22 - 10
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -224,27 +224,36 @@
     <Compile Include="..\MediaBrowser.Model\IO\IZipClient.cs">
       <Link>IO\IZipClient.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelGuide.cs">
-      <Link>LiveTv\ChannelGuide.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelInfoDto.cs">
       <Link>LiveTv\ChannelInfoDto.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelQuery.cs">
+      <Link>LiveTv\ChannelQuery.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelType.cs">
       <Link>LiveTv\ChannelType.cs</Link>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\LiveTvServiceInfo.cs">
       <Link>LiveTv\LiveTvServiceInfo.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramInfo.cs">
-      <Link>LiveTv\ProgramInfo.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramInfoDto.cs">
+      <Link>LiveTv\ProgramInfoDto.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfo.cs">
-      <Link>LiveTv\RecordingInfo.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramQuery.cs">
+      <Link>LiveTv\ProgramQuery.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfoDto.cs">
+      <Link>LiveTv\RecordingInfoDto.cs</Link>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingQuery.cs">
       <Link>LiveTv\RecordingQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingStatus.cs">
+      <Link>LiveTv\RecordingStatus.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\TimerInfoDto.cs">
+      <Link>LiveTv\TimerInfoDto.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Logging\ILogger.cs">
       <Link>Logging\ILogger.cs</Link>
     </Compile>
@@ -311,6 +320,9 @@
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Querying\EpisodeQuery.cs">
+      <Link>Querying\EpisodeQuery.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ItemCountsQuery.cs">
       <Link>Querying\ItemCountsQuery.cs</Link>
     </Compile>
@@ -323,9 +335,6 @@
     <Compile Include="..\MediaBrowser.Model\Querying\ItemQuery.cs">
       <Link>Querying\ItemQuery.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Querying\ItemReviewsResult.cs">
-      <Link>Querying\ItemReviewsResult.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ItemsByNameQuery.cs">
       <Link>Querying\ItemsByNameQuery.cs</Link>
     </Compile>
@@ -341,6 +350,9 @@
     <Compile Include="..\MediaBrowser.Model\Querying\PersonsQuery.cs">
       <Link>Querying\PersonsQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Querying\QueryResult.cs">
+      <Link>Querying\QueryResult.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\SessionQuery.cs">
       <Link>Querying\SessionQuery.cs</Link>
     </Compile>

+ 22 - 10
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -211,27 +211,36 @@
     <Compile Include="..\MediaBrowser.Model\IO\IZipClient.cs">
       <Link>IO\IZipClient.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelGuide.cs">
-      <Link>LiveTv\ChannelGuide.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelInfoDto.cs">
       <Link>LiveTv\ChannelInfoDto.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelQuery.cs">
+      <Link>LiveTv\ChannelQuery.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\ChannelType.cs">
       <Link>LiveTv\ChannelType.cs</Link>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\LiveTvServiceInfo.cs">
       <Link>LiveTv\LiveTvServiceInfo.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramInfo.cs">
-      <Link>LiveTv\ProgramInfo.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramInfoDto.cs">
+      <Link>LiveTv\ProgramInfoDto.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfo.cs">
-      <Link>LiveTv\RecordingInfo.cs</Link>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramQuery.cs">
+      <Link>LiveTv\ProgramQuery.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfoDto.cs">
+      <Link>LiveTv\RecordingInfoDto.cs</Link>
     </Compile>
     <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingQuery.cs">
       <Link>LiveTv\RecordingQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingStatus.cs">
+      <Link>LiveTv\RecordingStatus.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.Model\LiveTv\TimerInfoDto.cs">
+      <Link>LiveTv\TimerInfoDto.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Logging\ILogger.cs">
       <Link>Logging\ILogger.cs</Link>
     </Compile>
@@ -298,6 +307,9 @@
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Querying\EpisodeQuery.cs">
+      <Link>Querying\EpisodeQuery.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ItemCountsQuery.cs">
       <Link>Querying\ItemCountsQuery.cs</Link>
     </Compile>
@@ -310,9 +322,6 @@
     <Compile Include="..\MediaBrowser.Model\Querying\ItemQuery.cs">
       <Link>Querying\ItemQuery.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Querying\ItemReviewsResult.cs">
-      <Link>Querying\ItemReviewsResult.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ItemsByNameQuery.cs">
       <Link>Querying\ItemsByNameQuery.cs</Link>
     </Compile>
@@ -328,6 +337,9 @@
     <Compile Include="..\MediaBrowser.Model\Querying\PersonsQuery.cs">
       <Link>Querying\PersonsQuery.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Querying\QueryResult.cs">
+      <Link>Querying\QueryResult.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\SessionQuery.cs">
       <Link>Querying\SessionQuery.cs</Link>
     </Compile>

+ 15 - 1
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -83,7 +83,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="startIndex">The start index.</param>
         /// <param name="limit">The limit.</param>
         /// <returns>Task{ItemReviewsResult}.</returns>
-        Task<ItemReviewsResult> GetCriticReviews(string itemId, CancellationToken cancellationToken, int? startIndex = null, int? limit = null);
+        Task<QueryResult<ItemReview>> GetCriticReviews(string itemId, CancellationToken cancellationToken, int? startIndex = null, int? limit = null);
 
         /// <summary>
         /// Gets the theme songs async.
@@ -225,6 +225,20 @@ namespace MediaBrowser.Model.ApiClient
         /// <returns>Task{ItemCounts}.</returns>
         Task<ItemCounts> GetItemCountsAsync(ItemCountsQuery query);
 
+        /// <summary>
+        /// Gets the episodes asynchronous.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>Task{ItemsResult}.</returns>
+        Task<ItemsResult> GetEpisodesAsync(EpisodeQuery query);
+
+        /// <summary>
+        /// Gets the seasons asynchronous.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>Task{ItemsResult}.</returns>
+        Task<ItemsResult> GetSeasonsAsync(SeasonQuery query);
+        
         /// <summary>
         /// Queries for items
         /// </summary>

+ 6 - 0
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -312,6 +312,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The series id.</value>
         public string SeriesId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the season identifier.
+        /// </summary>
+        /// <value>The season identifier.</value>
+        public string SeasonId { get; set; }
+        
         /// <summary>
         /// Gets or sets the special feature count.
         /// </summary>

+ 4 - 1
MediaBrowser.Model/Dto/ItemByNameCounts.cs

@@ -1,4 +1,5 @@
-
+using System;
+
 namespace MediaBrowser.Model.Dto
 {
     /// <summary>
@@ -6,6 +7,8 @@ namespace MediaBrowser.Model.Dto
     /// </summary>
     public class ItemByNameCounts
     {
+        public Guid UserId { get; set; }
+
         /// <summary>
         /// Gets or sets the total count.
         /// </summary>

+ 0 - 23
MediaBrowser.Model/LiveTv/ChannelGuide.cs

@@ -1,23 +0,0 @@
-using System.Collections.Generic;
-
-namespace MediaBrowser.Model.LiveTv
-{
-    public class ChannelGuide
-    {
-        /// <summary>
-        /// Gets or sets the name of the service.
-        /// </summary>
-        /// <value>The name of the service.</value>
-        public string ServiceName { get; set; }
-
-        /// <summary>
-        /// ChannelId for the EPG.
-        /// </summary>
-        public string ChannelId { get; set; }
-
-        /// <summary>
-        /// List of all the programs for a specific channel
-        /// </summary>
-        public List<ProgramInfo> Programs { get; set; } 
-    }
-}

+ 34 - 1
MediaBrowser.Model/LiveTv/ChannelInfoDto.cs

@@ -1,4 +1,8 @@
-
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Model.LiveTv
 {
     /// <summary>
@@ -18,6 +22,12 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The identifier.</value>
         public string Id { 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 number.
         /// </summary>
@@ -35,5 +45,28 @@ namespace MediaBrowser.Model.LiveTv
         /// </summary>
         /// <value>The type of the channel.</value>
         public ChannelType ChannelType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public string Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        public string MediaType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user data.
+        /// </summary>
+        /// <value>The user data.</value>
+        public UserItemDataDto UserData { get; set; }
+
+        public ChannelInfoDto()
+        {
+            ImageTags = new Dictionary<ImageType, Guid>();
+        }
     }
 }

+ 27 - 0
MediaBrowser.Model/LiveTv/ChannelQuery.cs

@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Model.LiveTv
+{
+    /// <summary>
+    /// Class ChannelQuery.
+    /// </summary>
+    public class ChannelQuery
+    {
+        /// <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; }
+
+        /// <summary>
+        /// Gets or sets the user identifier.
+        /// </summary>
+        /// <value>The user identifier.</value>
+        public string UserId { get; set; }
+    }
+}

+ 0 - 37
MediaBrowser.Model/LiveTv/ProgramInfo.cs

@@ -1,37 +0,0 @@
-using System;
-
-namespace MediaBrowser.Model.LiveTv
-{
-    public class ProgramInfo
-    {
-        /// <summary>
-        /// Id of the program.
-        /// </summary>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Name of the program
-        /// </summary>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Description of the progam.
-        /// </summary>
-        public string Description { get; set; }
-
-        /// <summary>
-        /// The start date of the program, in UTC.
-        /// </summary>
-        public DateTime StartDate { get; set; }
-
-        /// <summary>
-        /// The end date of the program, in UTC.
-        /// </summary>
-        public DateTime EndDate { get; set; }
-
-        /// <summary>
-        /// Genre of the program.
-        /// </summary>
-        public string Genre { get; set; }
-    }
-}

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

@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.LiveTv
+{
+    public class ProgramInfoDto
+    {
+        /// <summary>
+        /// Id of the program.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the external identifier.
+        /// </summary>
+        /// <value>The external identifier.</value>
+        public string ExternalId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the channel identifier.
+        /// </summary>
+        /// <value>The channel identifier.</value>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the community rating.
+        /// </summary>
+        /// <value>The community rating.</value>
+        public float? CommunityRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        public string AspectRatio { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the official rating.
+        /// </summary>
+        /// <value>The official rating.</value>
+        public string OfficialRating { 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>
+        /// Name of the program
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Description of the progam.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the program, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the program, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Genre of the program.
+        /// </summary>
+        public List<string> Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets the quality.
+        /// </summary>
+        /// <value>The quality.</value>
+        public ProgramVideoQuality Quality { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio.
+        /// </summary>
+        /// <value>The audio.</value>
+        public ProgramAudio Audio { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the original air date.
+        /// </summary>
+        /// <value>The original air date.</value>
+        public DateTime? OriginalAirDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is repeat.
+        /// </summary>
+        /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+        public bool IsRepeat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode title.
+        /// </summary>
+        /// <value>The episode title.</value>
+        public string EpisodeTitle { get; set; }
+
+        public ProgramInfoDto()
+        {
+            Genres = new List<string>();
+        }
+    }
+
+    public enum ProgramVideoQuality
+    {
+        StandardDefinition,
+        HighDefinition
+    }
+
+    public enum ProgramAudio
+    {
+        Stereo
+    }
+}

+ 31 - 0
MediaBrowser.Model/LiveTv/ProgramQuery.cs

@@ -0,0 +1,31 @@
+namespace MediaBrowser.Model.LiveTv
+{
+    /// <summary>
+    /// Class ProgramQuery.
+    /// </summary>
+    public class ProgramQuery
+    {
+        /// <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 channel identifier.
+        /// </summary>
+        /// <value>The channel identifier.</value>
+        public string[] ChannelIdList { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user identifier.
+        /// </summary>
+        /// <value>The user identifier.</value>
+        public string UserId { get; set; }
+
+        public ProgramQuery()
+        {
+            ChannelIdList = new string[] { };
+        }
+    }
+}

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

@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.LiveTv
+{
+    public class RecordingInfoDto
+    {
+        /// <summary>
+        /// Id of the recording.
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the external identifier.
+        /// </summary>
+        /// <value>The external identifier.</value>
+        public string ExternalId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the program identifier.
+        /// </summary>
+        /// <value>The program identifier.</value>
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// ChannelId of the recording.
+        /// </summary>
+        public string ChannelId { get; set; }
+
+        /// <summary>
+        /// ChannelName of the recording.
+        /// </summary>
+        public string ChannelName { get; set; }
+
+        /// <summary>
+        /// Name of the recording.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Description of the recording.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// The start date of the recording, in UTC.
+        /// </summary>
+        public DateTime StartDate { get; set; }
+
+        /// <summary>
+        /// The end date of the recording, in UTC.
+        /// </summary>
+        public DateTime EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the status.
+        /// </summary>
+        /// <value>The status.</value>
+        public RecordingStatus Status { get; set; }
+
+        /// <summary>
+        /// Genre of the program.
+        /// </summary>
+        public List<string> Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is repeat.
+        /// </summary>
+        /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+        public bool IsRepeat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode title.
+        /// </summary>
+        /// <value>The episode title.</value>
+        public string EpisodeTitle { get; set; }
+
+        /// <summary>
+        /// Gets or sets the duration ms.
+        /// </summary>
+        /// <value>The duration ms.</value>
+        public int DurationMs { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the media.
+        /// </summary>
+        /// <value>The type of the media.</value>
+        public string MediaType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the channel.
+        /// </summary>
+        /// <value>The type of the channel.</value>
+        public ChannelType ChannelType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the official rating.
+        /// </summary>
+        /// <value>The official rating.</value>
+        public string OfficialRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the community rating.
+        /// </summary>
+        /// <value>The community rating.</value>
+        public float? CommunityRating { get; set; }
+
+        public RecordingInfoDto()
+        {
+            Genres = new List<string>();
+        }
+    }
+}

+ 24 - 3
MediaBrowser.Model/LiveTv/RecordingQuery.cs

@@ -6,9 +6,30 @@
     public class RecordingQuery
     {
         /// <summary>
-        /// Gets or sets a value indicating whether this instance has recorded.
+        /// Gets or sets the channel identifier.
         /// </summary>
-        /// <value><c>null</c> if [has recorded] contains no value, <c>true</c> if [has recorded]; otherwise, <c>false</c>.</value>
-        public bool? HasRecorded { get; set; }
+        /// <value>The channel identifier.</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; }
+    }
+
+    public class TimerQuery
+    {
+        /// <summary>
+        /// Gets or sets the channel identifier.
+        /// </summary>
+        /// <value>The channel identifier.</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; }
     }
 }

+ 22 - 0
MediaBrowser.Model/LiveTv/RecordingStatus.cs

@@ -0,0 +1,22 @@
+
+namespace MediaBrowser.Model.LiveTv
+{
+    public enum RecordingStatus
+    {
+        Pending,
+        InProgress,
+        Completed,
+        CompletedWithError,
+        Conflicted,
+        Deleted
+    }
+
+    public enum RecurrenceType
+    {
+        Manual,
+        NewProgramEventsOneChannel,
+        AllProgramEventsOneChannel,
+        NewProgramEventsAllChannels,
+        AllProgramEventsAllChannels
+    }
+}

+ 29 - 23
MediaBrowser.Model/LiveTv/RecordingInfo.cs → MediaBrowser.Model/LiveTv/TimerInfoDto.cs

@@ -1,15 +1,20 @@
 using System;
-using System.Collections.Generic;
 
 namespace MediaBrowser.Model.LiveTv
 {
-    public class RecordingInfo
+    public class TimerInfoDto
     {
         /// <summary>
         /// Id of the recording.
         /// </summary>
         public string Id { get; set; }
 
+        /// <summary>
+        /// Gets or sets the external identifier.
+        /// </summary>
+        /// <value>The external identifier.</value>
+        public string ExternalId { get; set; }
+
         /// <summary>
         /// ChannelId of the recording.
         /// </summary>
@@ -20,6 +25,12 @@ namespace MediaBrowser.Model.LiveTv
         /// </summary>
         public string ChannelName { get; set; }
 
+        /// <summary>
+        /// Gets or sets the program identifier.
+        /// </summary>
+        /// <value>The program identifier.</value>
+        public string ProgramId { get; set; }
+
         /// <summary>
         /// Name of the recording.
         /// </summary>
@@ -41,38 +52,33 @@ namespace MediaBrowser.Model.LiveTv
         public DateTime EndDate { get; set; }
 
         /// <summary>
-        /// Status of the recording.
-        /// </summary>
-        public string Status { get; set; } //TODO: Enum for status?? Difference NextPvr,Argus,...
-
-        /// <summary>
-        /// Quality of the Recording.
-        /// </summary>
-        public string Quality { get; set; } // TODO: Enum for quality?? Difference NextPvr,Argus,...
-
-        /// <summary>
-        /// Recurring recording?
+        /// Gets or sets the status.
         /// </summary>
-        public bool Recurring { get; set; }
+        /// <value>The status.</value>
+        public RecordingStatus Status { get; set; }
 
         /// <summary>
-        /// Parent recurring.
+        /// Gets or sets the series timer identifier.
         /// </summary>
-        public string RecurringParent { get; set; }
+        /// <value>The series timer identifier.</value>
+        public string SeriesTimerId { get; set; }
 
         /// <summary>
-        /// Start date for the recurring, in UTC.
+        /// Gets or sets the pre padding seconds.
         /// </summary>
-        public DateTime RecurrringStartDate { get; set; }
+        /// <value>The pre padding seconds.</value>
+        public int PrePaddingSeconds { get; set; }
 
         /// <summary>
-        /// End date for the recurring, in UTC
+        /// Gets or sets the post padding seconds.
         /// </summary>
-        public DateTime RecurringEndDate { get; set; }
+        /// <value>The post padding seconds.</value>
+        public int PostPaddingSeconds { get; set; }
 
         /// <summary>
-        /// When do we need the recording?
+        /// Gets or sets the duration ms.
         /// </summary>
-        public List<string> DayMask { get; set; }
+        /// <value>The duration ms.</value>
+        public int DurationMs { get; set; }
     }
-}
+}

+ 9 - 5
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -60,9 +60,13 @@
     <Compile Include="Dto\ItemCounts.cs" />
     <Compile Include="Dto\ItemIndex.cs" />
     <Compile Include="Entities\PackageReviewInfo.cs" />
-    <Compile Include="LiveTv\ChannelGuide.cs" />
-    <Compile Include="LiveTv\ProgramInfo.cs" />
+    <Compile Include="LiveTv\ChannelInfoDto.cs" />
+    <Compile Include="LiveTv\ChannelQuery.cs" />
+    <Compile Include="LiveTv\ProgramInfoDto.cs" />
+    <Compile Include="LiveTv\ProgramQuery.cs" />
     <Compile Include="LiveTv\RecordingQuery.cs" />
+    <Compile Include="LiveTv\RecordingStatus.cs" />
+    <Compile Include="LiveTv\TimerInfoDto.cs" />
     <Compile Include="Providers\ImageProviderInfo.cs" />
     <Compile Include="Providers\RemoteImageInfo.cs" />
     <Compile Include="Dto\StudioDto.cs" />
@@ -76,10 +80,9 @@
     <Compile Include="IO\IIsoManager.cs" />
     <Compile Include="IO\IIsoMount.cs" />
     <Compile Include="IO\IIsoMounter.cs" />
-    <Compile Include="LiveTv\ChannelInfoDto.cs" />
     <Compile Include="LiveTv\ChannelType.cs" />
     <Compile Include="LiveTv\LiveTvServiceInfo.cs" />
-    <Compile Include="LiveTv\RecordingInfo.cs" />
+    <Compile Include="LiveTv\RecordingInfoDto.cs" />
     <Compile Include="Net\WebSocketMessage.cs" />
     <Compile Include="Net\WebSocketMessageType.cs" />
     <Compile Include="Net\WebSocketState.cs" />
@@ -90,11 +93,12 @@
     <Compile Include="Notifications\NotificationsSummary.cs" />
     <Compile Include="Providers\RemoteImageResult.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
+    <Compile Include="Querying\EpisodeQuery.cs" />
     <Compile Include="Querying\ItemCountsQuery.cs" />
-    <Compile Include="Querying\ItemReviewsResult.cs" />
     <Compile Include="Querying\ItemsByNameQuery.cs" />
     <Compile Include="Entities\BaseItemInfo.cs" />
     <Compile Include="Querying\NextUpQuery.cs" />
+    <Compile Include="Querying\QueryResult.cs" />
     <Compile Include="Querying\SessionQuery.cs" />
     <Compile Include="Querying\SimilarItemsQuery.cs" />
     <Compile Include="Querying\UserQuery.cs" />

+ 45 - 0
MediaBrowser.Model/Querying/EpisodeQuery.cs

@@ -0,0 +1,45 @@
+
+namespace MediaBrowser.Model.Querying
+{
+    public class EpisodeQuery
+    {
+        public string UserId { get; set; }
+
+        public string SeasonId { get; set; }
+        
+        public string SeriesId { get; set; }
+
+        public bool? IsMissing { get; set; }
+
+        public bool? IsVirtualUnaired { get; set; }
+
+        public int? SeasonNumber { get; set; }
+
+        public ItemFields[] Fields { get; set; }
+
+        public EpisodeQuery()
+        {
+            Fields = new ItemFields[] { };
+        }
+    }
+
+    public class SeasonQuery
+    {
+        public string UserId { get; set; }
+
+        public string SeriesId { get; set; }
+
+        public bool? IsMissing { get; set; }
+
+        public bool? IsVirtualUnaired { get; set; }
+
+        public ItemFields[] Fields { get; set; }
+
+        public bool? IsSpecialSeason { get; set; }
+
+        public SeasonQuery()
+        {
+            Fields = new ItemFields[] { };
+        }
+    }
+}

+ 4 - 0
MediaBrowser.Model/Querying/ItemQuery.cs

@@ -266,6 +266,10 @@ namespace MediaBrowser.Model.Querying
         public double? MinCriticRating { get; set; }
 
         public int? AiredDuringSeason { get; set; }
+
+        public DateTime? MinPremiereDate { get; set; }
+
+        public DateTime? MaxPremiereDate { get; set; }
         
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemQuery" /> class.

+ 1 - 0
MediaBrowser.Model/Querying/ItemSortBy.cs

@@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Querying
     /// </summary>
     public static class ItemSortBy
     {
+        public const string AiredEpisodeOrder = "AiredEpisodeOrder";
         /// <summary>
         /// The album
         /// </summary>

+ 8 - 12
MediaBrowser.Model/Querying/ItemReviewsResult.cs → MediaBrowser.Model/Querying/QueryResult.cs

@@ -1,17 +1,13 @@
-using MediaBrowser.Model.Entities;
-
+
 namespace MediaBrowser.Model.Querying
 {
-    /// <summary>
-    /// Class ItemReviewsResult
-    /// </summary>
-    public class ItemReviewsResult
+    public class QueryResult<T>
     {
         /// <summary>
-        /// Gets or sets the item reviews.
+        /// Gets or sets the items.
         /// </summary>
-        /// <value>The item reviews.</value>
-        public ItemReview[] ItemReviews { get; set; }
+        /// <value>The items.</value>
+        public T[] Items { get; set; }
 
         /// <summary>
         /// The total number of records available
@@ -22,9 +18,9 @@ namespace MediaBrowser.Model.Querying
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemsResult" /> class.
         /// </summary>
-        public ItemReviewsResult()
+        public QueryResult()
         {
-            ItemReviews = new ItemReview[] { };
+            Items = new T[] { };
         }
-   }
+    }
 }

+ 12 - 0
MediaBrowser.Model/System/SystemInfo.cs

@@ -92,6 +92,18 @@ namespace MediaBrowser.Model.System
         /// <value>The program data path.</value>
         public string ProgramDataPath { get; set; }
 
+        /// <summary>
+        /// Gets or sets the items by name path.
+        /// </summary>
+        /// <value>The items by name path.</value>
+        public string ItemsByNamePath { get; set; }
+
+        /// <summary>
+        /// Gets or sets the log path.
+        /// </summary>
+        /// <value>The log path.</value>
+        public string LogPath { get; set; }
+        
         /// <summary>
         /// Gets or sets the HTTP server port number.
         /// </summary>

Vissa filer visades inte eftersom för många filer har ändrats