소스 검색

Merge pull request #1005 from MediaBrowser/dev

3.0.5518.0
Luke 10 년 전
부모
커밋
4cc3b2f0cc
100개의 변경된 파일2003개의 추가작업 그리고 1064개의 파일을 삭제
  1. 9 7
      MediaBrowser.Api/ApiEntryPoint.cs
  2. 75 15
      MediaBrowser.Api/BaseApiService.cs
  3. 13 1
      MediaBrowser.Api/BrandingService.cs
  4. 1 2
      MediaBrowser.Api/ConfigurationService.cs
  5. 8 8
      MediaBrowser.Api/ConnectService.cs
  6. 68 15
      MediaBrowser.Api/Dlna/DlnaServerService.cs
  7. 11 8
      MediaBrowser.Api/GamesService.cs
  8. 1 28
      MediaBrowser.Api/IHasDtoOptions.cs
  9. 6 11
      MediaBrowser.Api/Images/ImageService.cs
  10. 2 1
      MediaBrowser.Api/ItemLookupService.cs
  11. 1 1
      MediaBrowser.Api/ItemRefreshService.cs
  12. 1 1
      MediaBrowser.Api/ItemUpdateService.cs
  13. 1 1
      MediaBrowser.Api/Library/LibraryHelpers.cs
  14. 87 68
      MediaBrowser.Api/Library/LibraryService.cs
  15. 5 29
      MediaBrowser.Api/Library/LibraryStructureService.cs
  16. 6 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  17. 1 1
      MediaBrowser.Api/Movies/CollectionService.cs
  18. 12 13
      MediaBrowser.Api/Movies/MoviesService.cs
  19. 5 3
      MediaBrowser.Api/Movies/TrailersService.cs
  20. 7 5
      MediaBrowser.Api/Music/AlbumsService.cs
  21. 11 14
      MediaBrowser.Api/Music/InstantMixService.cs
  22. 130 14
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  23. 0 186
      MediaBrowser.Api/Playback/BifService.cs
  24. 5 6
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  25. 15 21
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  26. 2 6
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  27. 6 6
      MediaBrowser.Api/Playback/Hls/MpegDashService.cs
  28. 3 4
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  29. 2 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  30. 4 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  31. 3 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  32. 4 1
      MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
  33. 4 3
      MediaBrowser.Api/PlaylistService.cs
  34. 3 5
      MediaBrowser.Api/PluginService.cs
  35. 9 0
      MediaBrowser.Api/Reports/ReportFieldType.cs
  36. 33 0
      MediaBrowser.Api/Reports/ReportRequests.cs
  37. 16 0
      MediaBrowser.Api/Reports/ReportResult.cs
  38. 64 0
      MediaBrowser.Api/Reports/ReportsService.cs
  39. 23 2
      MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
  40. 5 10
      MediaBrowser.Api/SearchService.cs
  41. 1 2
      MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs
  42. 8 3
      MediaBrowser.Api/Session/SessionsService.cs
  43. 8 7
      MediaBrowser.Api/SimilarItemsHelper.cs
  44. 3 0
      MediaBrowser.Api/StartupWizardService.cs
  45. 120 0
      MediaBrowser.Api/Sync/SyncJobWebSocketListener.cs
  46. 101 0
      MediaBrowser.Api/Sync/SyncJobsWebSocketListener.cs
  47. 93 5
      MediaBrowser.Api/Sync/SyncService.cs
  48. 52 22
      MediaBrowser.Api/TvShowsService.cs
  49. 3 3
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  50. 41 27
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  51. 1 1
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  52. 1 1
      MediaBrowser.Api/UserLibrary/GenresService.cs
  53. 14 39
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  54. 1 1
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  55. 1 2
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  56. 1 1
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  57. 19 87
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  58. 1 1
      MediaBrowser.Api/UserLibrary/YearsService.cs
  59. 110 6
      MediaBrowser.Api/UserService.cs
  60. 1 2
      MediaBrowser.Api/VideosService.cs
  61. 1 1
      MediaBrowser.Common.Implementations/BaseApplicationHost.cs
  62. 1 1
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  63. 23 3
      MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs
  64. 6 6
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  65. 73 3
      MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs
  66. 110 40
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  67. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  68. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  69. 6 3
      MediaBrowser.Common.Implementations/Updates/InstallationManager.cs
  70. 2 2
      MediaBrowser.Common.Implementations/packages.config
  71. 0 30
      MediaBrowser.Common/Extensions/BaseExtensions.cs
  72. 28 0
      MediaBrowser.Common/IO/IFileSystem.cs
  73. 7 0
      MediaBrowser.Common/Net/INetworkManager.cs
  74. 29 3
      MediaBrowser.Controller/Channels/Channel.cs
  75. 7 2
      MediaBrowser.Controller/Channels/ChannelAudioItem.cs
  76. 9 5
      MediaBrowser.Controller/Channels/ChannelFolderItem.cs
  77. 8 3
      MediaBrowser.Controller/Channels/ChannelVideoItem.cs
  78. 7 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  79. 10 0
      MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs
  80. 4 1
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  81. 2 1
      MediaBrowser.Controller/Dlna/IDlnaManager.cs
  82. 7 0
      MediaBrowser.Controller/Dlna/IMediaReceiverRegistrar.cs
  83. 1 0
      MediaBrowser.Controller/Dto/DtoOptions.cs
  84. 11 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  85. 0 19
      MediaBrowser.Controller/Entities/AdultVideo.cs
  86. 6 1
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  87. 20 4
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  88. 8 13
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  89. 38 60
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  90. 14 2
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  91. 99 16
      MediaBrowser.Controller/Entities/BaseItem.cs
  92. 5 0
      MediaBrowser.Controller/Entities/BasePluginFolder.cs
  93. 8 0
      MediaBrowser.Controller/Entities/Book.cs
  94. 10 9
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  95. 151 111
      MediaBrowser.Controller/Entities/Folder.cs
  96. 10 3
      MediaBrowser.Controller/Entities/Game.cs
  97. 15 2
      MediaBrowser.Controller/Entities/GameGenre.cs
  98. 2 2
      MediaBrowser.Controller/Entities/GameSystem.cs
  99. 16 3
      MediaBrowser.Controller/Entities/Genre.cs
  100. 5 0
      MediaBrowser.Controller/Entities/IHasMetadata.cs

+ 9 - 7
MediaBrowser.Api/ApiEntryPoint.cs

@@ -1,10 +1,10 @@
 using MediaBrowser.Api.Playback;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Session;
 using System;
@@ -39,6 +39,7 @@ namespace MediaBrowser.Api
         private readonly IServerConfigurationManager _config;
 
         private readonly ISessionManager _sessionManager;
+        private readonly IFileSystem _fileSystem;
 
         public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
 
@@ -48,11 +49,12 @@ namespace MediaBrowser.Api
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="config">The configuration.</param>
-        public ApiEntryPoint(ILogger logger, ISessionManager sessionManager, IServerConfigurationManager config)
+        public ApiEntryPoint(ILogger logger, ISessionManager sessionManager, IServerConfigurationManager config, IFileSystem fileSystem)
         {
             Logger = logger;
             _sessionManager = sessionManager;
             _config = config;
+            _fileSystem = fileSystem;
 
             Instance = this;
         }
@@ -86,12 +88,12 @@ namespace MediaBrowser.Api
         /// </summary>
         private void DeleteEncodedMediaCache()
         {
-            var path = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, EncodingContext.Streaming.ToString().ToLower());
+            var path = _config.ApplicationPaths.TranscodingTempPath;
 
             foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
                 .ToList())
             {
-                File.Delete(file);
+                _fileSystem.DeleteFile(file);
             }
         }
 
@@ -462,7 +464,7 @@ namespace MediaBrowser.Api
         /// <param name="outputFilePath">The output file path.</param>
         private void DeleteProgressivePartialStreamFiles(string outputFilePath)
         {
-            File.Delete(outputFilePath);
+            _fileSystem.DeleteFile(outputFilePath);
         }
 
         /// <summary>
@@ -479,13 +481,13 @@ namespace MediaBrowser.Api
                 .ToList();
 
             Exception e = null;
-
+            
             foreach (var file in filesToDelete)
             {
                 try
                 {
                     Logger.Info("Deleting HLS file {0}", file);
-                    File.Delete(file);
+                    _fileSystem.DeleteFile(file);
                 }
                 catch (DirectoryNotFoundException)
                 {

+ 75 - 15
MediaBrowser.Api/BaseApiService.cs

@@ -1,9 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
+using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
@@ -21,7 +24,7 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <value>The logger.</value>
         public ILogger Logger { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the HTTP result factory.
         /// </summary>
@@ -35,6 +38,7 @@ namespace MediaBrowser.Api
         public IRequest Request { get; set; }
 
         public ISessionContext SessionContext { get; set; }
+        public IAuthorizationContext AuthorizationContext { get; set; }
 
         public string GetHeader(string name)
         {
@@ -109,6 +113,37 @@ namespace MediaBrowser.Api
         private readonly char[] _dashReplaceChars = { '?', '/', '&' };
         private const char SlugChar = '-';
 
+        protected DtoOptions GetDtoOptions(object request)
+        {
+            var options = new DtoOptions();
+
+            options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
+
+            var hasFields = request as IHasItemFields;
+            if (hasFields != null)
+            {
+                options.Fields = hasFields.GetItemFields().ToList();
+            }
+
+            var hasDtoOptions = request as IHasDtoOptions;
+            if (hasDtoOptions != null)
+            {
+                options.EnableImages = hasDtoOptions.EnableImages ?? true;
+
+                if (hasDtoOptions.ImageTypeLimit.HasValue)
+                {
+                    options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
+                }
+
+                if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
+                {
+                    options.ImageTypes = (hasDtoOptions.EnableImageTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToList();
+                }
+            }
+
+            return options;
+        }
+
         protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
         {
             return libraryManager.GetArtist(DeSlugArtistName(name, libraryManager));
@@ -139,11 +174,11 @@ namespace MediaBrowser.Api
             return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
         }
 
-        protected IEnumerable<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
+        protected IList<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId, Func<BaseItem,bool> filter)
         {
             if (!string.IsNullOrEmpty(parentId))
             {
-                var folder = (Folder) libraryManager.GetItemById(new Guid(parentId));
+                var folder = (Folder)libraryManager.GetItemById(new Guid(parentId));
 
                 if (userId.HasValue)
                 {
@@ -154,10 +189,13 @@ namespace MediaBrowser.Api
                         throw new ArgumentException("User not found");
                     }
 
-                    return folder.GetRecursiveChildren(user);
+                    return folder
+                        .GetRecursiveChildren(user, filter)
+                        .ToList();
                 }
 
-                return folder.GetRecursiveChildren();
+                return folder
+                    .GetRecursiveChildren(filter);
             }
             if (userId.HasValue)
             {
@@ -168,10 +206,16 @@ namespace MediaBrowser.Api
                     throw new ArgumentException("User not found");
                 }
 
-                return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
+                return userManager
+                    .GetUserById(userId.Value)
+                    .RootFolder
+                    .GetRecursiveChildren(user, filter)
+                    .ToList();
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren();
+            return libraryManager
+                .RootFolder
+                .GetRecursiveChildren(filter);
         }
 
         /// <summary>
@@ -187,8 +231,9 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.RecursiveChildren
-                .OfType<Audio>()
+            return libraryManager.RootFolder
+                .GetRecursiveChildren(i => i is IHasArtist)
+                .Cast<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .FirstOrDefault(i =>
@@ -229,8 +274,8 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren()
-                .OfType<Game>()
+            return libraryManager.RootFolder
+                .GetRecursiveChildren(i => i is Game)
                 .SelectMany(i => i.Genres)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .FirstOrDefault(i =>
@@ -252,7 +297,8 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren()
+            return libraryManager.RootFolder
+                .GetRecursiveChildren()
                 .SelectMany(i => i.Studios)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .FirstOrDefault(i =>
@@ -274,7 +320,8 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren()
+            return libraryManager.RootFolder
+                .GetRecursiveChildren()
                 .SelectMany(i => i.People)
                 .Select(i => i.Name)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
@@ -287,6 +334,20 @@ namespace MediaBrowser.Api
                 }) ?? name;
         }
 
+        protected string GetPathValue(int index)
+        {
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var first = pathInfo.GetArgumentValue<string>(0);
+
+            // backwards compatibility
+            if (string.Equals(first, "mediabrowser", StringComparison.OrdinalIgnoreCase))
+            {
+                index++;
+            }
+
+            return pathInfo.GetArgumentValue<string>(index);
+        }
+
         /// <summary>
         /// Gets the name of the item by.
         /// </summary>
@@ -294,7 +355,6 @@ namespace MediaBrowser.Api
         /// <param name="type">The type.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <returns>Task{BaseItem}.</returns>
-        /// <exception cref="System.ArgumentException"></exception>
         protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager)
         {
             BaseItem item;

+ 13 - 1
MediaBrowser.Api/BrandingService.cs

@@ -8,7 +8,12 @@ namespace MediaBrowser.Api
     public class GetBrandingOptions : IReturn<BrandingOptions>
     {
     }
-    
+
+    [Route("/Branding/Css", "GET", Summary = "Gets custom css")]
+    public class GetBrandingCss
+    {
+    }
+
     public class BrandingService : BaseApiService
     {
         private readonly IConfigurationManager _config;
@@ -24,5 +29,12 @@ namespace MediaBrowser.Api
 
             return ToOptimizedResult(result);
         }
+
+        public object Get(GetBrandingCss request)
+        {
+            var result = _config.GetConfiguration<BrandingOptions>("branding");
+
+            return ResultFactory.GetResult(result.CustomCss, "text/css");
+        }
     }
 }

+ 1 - 2
MediaBrowser.Api/ConfigurationService.cs

@@ -143,8 +143,7 @@ namespace MediaBrowser.Api
 
         public void Post(UpdateNamedConfiguration request)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var key = pathInfo.GetArgumentValue<string>(2);
+            var key = GetPathValue(2);
 
             var configurationType = _configurationManager.GetConfigurationType(key);
             var configuration = _jsonSerializer.DeserializeFromStream(request.RequestStream, configurationType);

+ 8 - 8
MediaBrowser.Api/ConnectService.cs

@@ -39,11 +39,11 @@ namespace MediaBrowser.Api
         [ApiMember(Name = "SendingUserId", Description = "Sending User Id", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
         public string SendingUserId { get; set; }
 
-        [ApiMember(Name = "ExcludeLibraries", Description = "ExcludeLibraries", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string ExcludedLibraries { get; set; }
+        [ApiMember(Name = "EnabledLibraries", Description = "EnabledLibraries", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
+        public string EnabledLibraries { get; set; }
 
-        [ApiMember(Name = "ExcludedChannels", Description = "ExcludedChannels", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string ExcludedChannels { get; set; }
+        [ApiMember(Name = "EnabledChannels", Description = "EnabledChannels", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
+        public string EnabledChannels { get; set; }
 
         [ApiMember(Name = "EnableLiveTv", Description = "EnableLiveTv", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
         public bool EnableLiveTv { get; set; }
@@ -91,12 +91,12 @@ namespace MediaBrowser.Api
 
         public object Post(CreateConnectInvite request)
         {
-            var excludeLibraries = (request.ExcludedLibraries ?? string.Empty)
+            var enabledLibraries = (request.EnabledLibraries ?? string.Empty)
                 .Split(',')
                 .Where(i => !string.IsNullOrWhiteSpace(i))
                 .ToArray();
 
-            var excludedChannels = (request.ExcludedChannels ?? string.Empty)
+            var enabledChannels = (request.EnabledChannels ?? string.Empty)
                 .Split(',')
                 .Where(i => !string.IsNullOrWhiteSpace(i))
                 .ToArray();
@@ -105,8 +105,8 @@ namespace MediaBrowser.Api
             {
                 ConnectUserName = request.ConnectUsername,
                 SendingUserId = request.SendingUserId,
-                ExcludedLibraries = excludeLibraries,
-                ExcludedChannels = excludedChannels,
+                EnabledLibraries = enabledLibraries,
+                EnabledChannels = enabledChannels,
                 EnableLiveTv = request.EnableLiveTv
             });
         }

+ 68 - 15
MediaBrowser.Api/Dlna/DlnaServerService.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Controller.Dlna;
 using ServiceStack;
-using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
@@ -19,19 +18,31 @@ namespace MediaBrowser.Api.Dlna
         public string UuId { get; set; }
     }
 
-    [Route("/Dlna/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
-    [Route("/Dlna/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
+    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
+    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
     public class GetContentDirectory
     {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UuId { get; set; }
     }
 
-    [Route("/Dlna/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
-    [Route("/Dlna/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
+    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
+    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
     public class GetConnnectionManager
     {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UuId { get; set; }
+    }
+
+    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
+    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
+    public class GetMediaReceiverRegistrar
+    {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UuId { get; set; }
     }
 
-    [Route("/Dlna/contentdirectory/{UuId}/control", "POST", Summary = "Processes a control request")]
+    [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
     public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
     {
         [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
@@ -40,7 +51,7 @@ namespace MediaBrowser.Api.Dlna
         public Stream RequestStream { get; set; }
     }
 
-    [Route("/Dlna/connectionmanager/{UuId}/control", "POST", Summary = "Processes a control request")]
+    [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
     public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
     {
         [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
@@ -49,23 +60,43 @@ namespace MediaBrowser.Api.Dlna
         public Stream RequestStream { get; set; }
     }
 
-    [Route("/Dlna/contentdirectory/{UuId}/events", Summary = "Processes an event subscription request")]
-    public class ProcessContentDirectoryEventRequest
+    [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
+    public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
     {
         [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string UuId { get; set; }
+
+        public Stream RequestStream { get; set; }
     }
 
-    [Route("/Dlna/connectionmanager/{UuId}/events", Summary = "Processes an event subscription request")]
+    [Route("/Dlna/{UuId}/mediareceiverregistrar/events", Summary = "Processes an event subscription request")]
+    public class ProcessMediaReceiverRegistrarEventRequest
+    {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,POST")]
+        public string UuId { get; set; }
+    }
+
+    [Route("/Dlna/{UuId}/contentdirectory/events", Summary = "Processes an event subscription request")]
+    public class ProcessContentDirectoryEventRequest
+    {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,POST")]
+        public string UuId { get; set; }
+    }
+
+    [Route("/Dlna/{UuId}/connectionmanager/events", Summary = "Processes an event subscription request")]
     public class ProcessConnectionManagerEventRequest
     {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,POST")]
         public string UuId { get; set; }
     }
 
+    [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
     [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
     public class GetIcon
     {
+        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UuId { get; set; }
+        
         [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Filename { get; set; }
     }
@@ -75,17 +106,21 @@ namespace MediaBrowser.Api.Dlna
         private readonly IDlnaManager _dlnaManager;
         private readonly IContentDirectory _contentDirectory;
         private readonly IConnectionManager _connectionManager;
+        private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
 
-        public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager)
+        public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IMediaReceiverRegistrar mediaReceiverRegistrar)
         {
             _dlnaManager = dlnaManager;
             _contentDirectory = contentDirectory;
             _connectionManager = connectionManager;
+            _mediaReceiverRegistrar = mediaReceiverRegistrar;
         }
 
         public object Get(GetDescriptionXml request)
         {
-            var xml = _dlnaManager.GetServerDescriptionXml(GetRequestHeaders(), request.UuId);
+            var url = Request.AbsoluteUri;
+            var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
+            var xml = _dlnaManager.GetServerDescriptionXml(GetRequestHeaders(), request.UuId, serverAddress);
 
             return ResultFactory.GetResult(xml, "text/xml");
         }
@@ -97,6 +132,13 @@ namespace MediaBrowser.Api.Dlna
             return ResultFactory.GetResult(xml, "text/xml");
         }
 
+        public object Get(GetMediaReceiverRegistrar request)
+        {
+            var xml = _mediaReceiverRegistrar.GetServiceXml(GetRequestHeaders());
+
+            return ResultFactory.GetResult(xml, "text/xml");
+        }
+
         public object Get(GetConnnectionManager request)
         {
             var xml = _connectionManager.GetServiceXml(GetRequestHeaders());
@@ -104,6 +146,13 @@ namespace MediaBrowser.Api.Dlna
             return ResultFactory.GetResult(xml, "text/xml");
         }
 
+        public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
+        {
+            var response = await PostAsync(request.RequestStream, _mediaReceiverRegistrar).ConfigureAwait(false);
+
+            return ResultFactory.GetResult(response.Xml, "text/xml");
+        }
+
         public async Task<object> Post(ProcessContentDirectoryControlRequest request)
         {
             var response = await PostAsync(request.RequestStream, _contentDirectory).ConfigureAwait(false);
@@ -120,8 +169,7 @@ namespace MediaBrowser.Api.Dlna
 
         private async Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = pathInfo.GetArgumentValue<string>(2);
+            var id = GetPathValue(2);
 
             using (var reader = new StreamReader(requestStream))
             {
@@ -172,6 +220,11 @@ namespace MediaBrowser.Api.Dlna
             return ProcessEventRequest(_connectionManager);
         }
 
+        public object Any(ProcessMediaReceiverRegistrarEventRequest request)
+        {
+            return ProcessEventRequest(_mediaReceiverRegistrar);
+        }
+
         private object ProcessEventRequest(IEventManager eventManager)
         {
             var subscriptionId = GetHeader("SID");

+ 11 - 8
MediaBrowser.Api/GamesService.cs

@@ -102,8 +102,8 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         public object Get(GetGameSystemSummaries request)
         {
-            var gameSystems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager)
-                .OfType<GameSystem>()
+            var gameSystems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, i => i is GameSystem)
+                .Cast<GameSystem>()
                 .ToList();
 
             var user = request.UserId == null ? null : _userManager.GetUserById(request.UserId.Value);
@@ -119,9 +119,8 @@ namespace MediaBrowser.Api
 
         public object Get(GetPlayerIndex request)
         {
-            var games = GetAllLibraryItems(request.UserId, _userManager, _libraryManager)
-                .OfType<Game>()
-                .ToList();
+            var games = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, i => i is Game)
+                .Cast<Game>();
 
             var lookup = games
                 .ToLookup(i => i.PlayersSupported ?? -1)
@@ -150,9 +149,11 @@ namespace MediaBrowser.Api
                 DisplayName = system.Name
             };
 
-            var items = user == null ? system.RecursiveChildren : system.GetRecursiveChildren(user);
+            var items = user == null ? 
+                system.GetRecursiveChildren(i => i is Game) :
+                system.GetRecursiveChildren(user, i => i is Game);
 
-            var games = items.OfType<Game>().ToList();
+            var games = items.Cast<Game>().ToList();
 
             summary.ClientInstalledGameCount = games.Count(i => i.IsPlaceHolder);
 
@@ -172,7 +173,9 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         public object Get(GetSimilarGames request)
         {
-            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
+            var dtoOptions = GetDtoOptions(request);
+
+            var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager,
                 _itemRepo,
                 _libraryManager,
                 _userDataRepository,

+ 1 - 28
MediaBrowser.Api/IHasDtoOptions.cs

@@ -1,8 +1,4 @@
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Linq;
-
+
 namespace MediaBrowser.Api
 {
     public interface IHasDtoOptions : IHasItemFields
@@ -13,27 +9,4 @@ namespace MediaBrowser.Api
 
         string EnableImageTypes { get; set; }
     }
-
-    public static class HasDtoOptionsExtensions
-    {
-        public static DtoOptions GetDtoOptions(this IHasDtoOptions request)
-        {
-            var options = new DtoOptions();
-
-            options.Fields = request.GetItemFields().ToList();
-            options.EnableImages = request.EnableImages ?? true;
-
-            if (request.ImageTypeLimit.HasValue)
-            {
-                options.ImageTypeLimit = request.ImageTypeLimit.Value;
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.EnableImageTypes))
-            {
-                options.ImageTypes = (request.EnableImageTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToList();
-            }
-
-            return options;
-        }
-    }
 }

+ 6 - 11
MediaBrowser.Api/Images/ImageService.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using ServiceStack;
-using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
@@ -396,8 +395,7 @@ namespace MediaBrowser.Api.Images
 
         public object Get(GetItemByNameImage request)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var type = pathInfo.GetArgumentValue<string>(0);
+            var type = GetPathValue(0);
 
             var item = GetItemByName(request.Name, type, _libraryManager);
 
@@ -406,8 +404,7 @@ namespace MediaBrowser.Api.Images
 
         public object Head(GetItemByNameImage request)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var type = pathInfo.GetArgumentValue<string>(0);
+            var type = GetPathValue(0);
 
             var item = GetItemByName(request.Name, type, _libraryManager);
 
@@ -420,10 +417,9 @@ namespace MediaBrowser.Api.Images
         /// <param name="request">The request.</param>
         public void Post(PostUserImage request)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+            var id = new Guid(GetPathValue(1));
 
-            request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(3), true);
+            request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true);
 
             var item = _userManager.GetUserById(id);
 
@@ -438,10 +434,9 @@ namespace MediaBrowser.Api.Images
         /// <param name="request">The request.</param>
         public void Post(PostItemImage request)
         {
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+            var id = new Guid(GetPathValue(1));
 
-            request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(3), true);
+            request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true);
 
             var item = _libraryManager.GetItemById(id);
 

+ 2 - 1
MediaBrowser.Api/ItemLookupService.cs

@@ -205,7 +205,8 @@ namespace MediaBrowser.Api
                 Logger = Logger,
                 Request = Request,
                 ResultFactory = ResultFactory,
-                SessionContext = SessionContext
+                SessionContext = SessionContext,
+                AuthorizationContext = AuthorizationContext
             };
 
             service.Post(new RefreshItem

+ 1 - 1
MediaBrowser.Api/ItemRefreshService.cs

@@ -51,7 +51,7 @@ namespace MediaBrowser.Api
             var cancellationToken = CancellationToken.None;
 
             var albums = _libraryManager.RootFolder
-                                        .RecursiveChildren
+                                        .GetRecursiveChildren()
                                         .OfType<MusicAlbum>()
                                         .Where(i => i.HasArtist(item.Name))
                                         .ToList();

+ 1 - 1
MediaBrowser.Api/ItemUpdateService.cs

@@ -215,7 +215,7 @@ namespace MediaBrowser.Api
             {
                 var folder = (Folder)item;
 
-                foreach (var child in folder.RecursiveChildren.ToList())
+                foreach (var child in folder.GetRecursiveChildren())
                 {
                     child.IsLocked = newLockData;
                     await child.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);

+ 1 - 1
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Library
 
             if (!string.IsNullOrEmpty(shortcut))
             {
-                File.Delete(shortcut);
+                fileSystem.DeleteFile(shortcut);
             }
         }
 

+ 87 - 68
MediaBrowser.Api/Library/LibraryService.cs

@@ -5,10 +5,8 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -226,6 +224,18 @@ namespace MediaBrowser.Api.Library
         public string TvdbId { get; set; }
     }
 
+    [Route("/Items/{Id}/Download", "GET", Summary = "Downloads item media")]
+    [Authenticated(Roles = "download")]
+    public class GetDownload
+    {
+        /// <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; }
+    }
+
     /// <summary>
     /// Class LibraryService
     /// </summary>
@@ -272,8 +282,8 @@ namespace MediaBrowser.Api.Library
                 items = items.Where(i => i.IsHidden == val).ToList();
             }
 
-            var dtoOptions = new DtoOptions();
-            
+            var dtoOptions = GetDtoOptions(request);
+
             var result = new ItemsResult
             {
                 TotalRecordCount = items.Count,
@@ -289,6 +299,28 @@ namespace MediaBrowser.Api.Library
             Task.Run(() => _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None));
         }
 
+        public object Get(GetDownload request)
+        {
+            var item = _libraryManager.GetItemById(request.Id);
+
+            if (!item.CanDelete())
+            {
+                throw new ArgumentException("Item does not support downloading");
+            }
+
+            var headers = new Dictionary<string, string>();
+
+            // Quotes are valid in linux. They'll possibly cause issues here
+            var filename = Path.GetFileName(item.Path).Replace("\"", string.Empty);
+            headers["Content-Disposition"] = string.Format("attachment; filename=\"{0}\"", filename);
+
+            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                Path = item.Path,
+                ResponseHeaders = headers
+            });
+        }
+
         public object Get(GetFile request)
         {
             var item = _libraryManager.GetItemById(request.Id);
@@ -344,10 +376,10 @@ namespace MediaBrowser.Api.Library
 
             var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             BaseItem parent = item.Parent;
-            
+
             while (parent != null)
             {
                 if (user != null)
@@ -392,52 +424,43 @@ namespace MediaBrowser.Api.Library
         /// <returns>System.Object.</returns>
         public object Get(GetItemCounts request)
         {
-            var items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager)
-                .Where(i => i.LocationType != LocationType.Virtual)
-                .ToList();
-
-            var filteredItems = request.UserId.HasValue ? FilterItems(items, request, request.UserId.Value).ToList() : items;
-
-            var albums = filteredItems.OfType<MusicAlbum>().ToList();
-            var episodes = filteredItems.OfType<Episode>().ToList();
-            var games = filteredItems.OfType<Game>().ToList();
-            var movies = filteredItems.OfType<Movie>().ToList();
-            var musicVideos = filteredItems.OfType<MusicVideo>().ToList();
-            var boxsets = filteredItems.OfType<BoxSet>().ToList();
-            var books = filteredItems.OfType<Book>().ToList();
-            var songs = filteredItems.OfType<Audio>().ToList();
-            var series = filteredItems.OfType<Series>().ToList();
+            var filteredItems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, i => i.LocationType != LocationType.Virtual && FilterItem(i, request, request.UserId));
 
             var counts = new ItemCounts
             {
-                AlbumCount = albums.Count,
-                EpisodeCount = episodes.Count,
-                GameCount = games.Count,
-                GameSystemCount = filteredItems.OfType<GameSystem>().Count(),
-                MovieCount = movies.Count,
-                SeriesCount = series.Count,
-                SongCount = songs.Count,
-                MusicVideoCount = musicVideos.Count,
-                BoxSetCount = boxsets.Count,
-                BookCount = books.Count,
-
-                UniqueTypes = items.Select(i => i.GetClientTypeName()).Distinct().ToList()
+                AlbumCount = filteredItems.Count(i => i is MusicAlbum),
+                EpisodeCount = filteredItems.Count(i => i is Episode),
+                GameCount = filteredItems.Count(i => i is Game),
+                GameSystemCount = filteredItems.Count(i => i is GameSystem),
+                MovieCount = filteredItems.Count(i => i is Movie),
+                SeriesCount = filteredItems.Count(i => i is Series),
+                SongCount = filteredItems.Count(i => i is Audio),
+                MusicVideoCount = filteredItems.Count(i => i is MusicVideo),
+                BoxSetCount = filteredItems.Count(i => i is BoxSet),
+                BookCount = filteredItems.Count(i => i is Book),
+
+                UniqueTypes = filteredItems.Select(i => i.GetClientTypeName()).Distinct().ToList()
             };
 
             return ToOptimizedSerializedResultUsingCache(counts);
         }
 
-        private IEnumerable<T> FilterItems<T>(IEnumerable<T> items, GetItemCounts request, Guid userId)
-            where T : BaseItem
+        private bool FilterItem(BaseItem item, GetItemCounts request, Guid? userId)
         {
-            if (request.IsFavorite.HasValue)
+            if (userId.HasValue)
             {
-                var val = request.IsFavorite.Value;
+                if (request.IsFavorite.HasValue)
+                {
+                    var val = request.IsFavorite.Value;
 
-                items = items.Where(i => _userDataManager.GetUserData(userId, i.GetUserDataKey()).IsFavorite == val);
+                    if (_userDataManager.GetUserData(userId.Value, item.GetUserDataKey()).IsFavorite != val)
+                    {
+                        return false;
+                    }
+                }
             }
 
-            return items;
+            return true;
         }
 
         /// <summary>
@@ -467,23 +490,9 @@ namespace MediaBrowser.Api.Library
             var auth = _authContext.GetAuthorizationInfo(Request);
             var user = _userManager.GetUserById(auth.UserId);
 
-            if (item is Playlist || item is BoxSet)
+            if (!item.CanDelete(user))
             {
-                // For now this is allowed if user can see the playlist
-            }
-            else if (item is ILiveTvRecording)
-            {
-                if (!user.Policy.EnableLiveTvManagement)
-                {
-                    throw new UnauthorizedAccessException();
-                }
-            }
-            else
-            {
-                if (!user.Policy.EnableContentDeletion)
-                {
-                    throw new UnauthorizedAccessException();
-                }
+                throw new UnauthorizedAccessException();
             }
 
             var task = _libraryManager.DeleteItem(item);
@@ -544,7 +553,7 @@ namespace MediaBrowser.Api.Library
                 ThemeSongsResult = themeSongs,
                 ThemeVideosResult = themeVideos,
 
-                SoundtrackSongsResult = GetSoundtrackSongs(request.Id, request.UserId, request.InheritFromParent)
+                SoundtrackSongsResult = GetSoundtrackSongs(request, request.Id, request.UserId, request.InheritFromParent)
             });
         }
 
@@ -597,7 +606,7 @@ namespace MediaBrowser.Api.Library
                 }
             }
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = themeSongIds.Select(_libraryManager.GetItemById)
                             .OrderBy(i => i.SortName)
@@ -667,7 +676,7 @@ namespace MediaBrowser.Api.Library
                 }
             }
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = themeVideoIds.Select(_libraryManager.GetItemById)
                             .OrderBy(i => i.SortName)
@@ -711,13 +720,24 @@ namespace MediaBrowser.Api.Library
 
         public object Get(GetYearIndex request)
         {
-            IEnumerable<BaseItem> items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager);
+            var includeTypes = string.IsNullOrWhiteSpace(request.IncludeItemTypes)
+             ? new string[] { }
+             : request.IncludeItemTypes.Split(',');
 
-            if (!string.IsNullOrEmpty(request.IncludeItemTypes))
+            Func<BaseItem, bool> filter = i =>
             {
-                var vals = request.IncludeItemTypes.Split(',');
-                items = items.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
-            }
+                if (includeTypes.Length > 0)
+                {
+                    if (!includeTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            };
+
+            IEnumerable<BaseItem> items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, filter);
 
             var lookup = items
                 .ToLookup(i => i.ProductionYear ?? -1)
@@ -732,23 +752,22 @@ namespace MediaBrowser.Api.Library
             return ToOptimizedSerializedResultUsingCache(lookup);
         }
 
-        public ThemeMediaResult GetSoundtrackSongs(string id, Guid? userId, bool inheritFromParent)
+        public ThemeMediaResult GetSoundtrackSongs(GetThemeMedia request, string id, Guid? userId, bool inheritFromParent)
         {
             var user = userId.HasValue ? _userManager.GetUserById(userId.Value) : null;
 
             var item = string.IsNullOrEmpty(id)
                            ? (userId.HasValue
                                   ? user.RootFolder
-                                  : (Folder)_libraryManager.RootFolder)
+                                  : _libraryManager.RootFolder)
                            : _libraryManager.GetItemById(id);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = GetSoundtrackSongIds(item, inheritFromParent)
                 .Select(_libraryManager.GetItemById)
                 .OfType<MusicAlbum>()
-                .SelectMany(i => i.RecursiveChildren)
-                .OfType<Audio>()
+                .SelectMany(i => i.GetRecursiveChildren(a => a is Audio))
                 .OrderBy(i => i.SortName)
                 .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
 

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

@@ -3,7 +3,6 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
@@ -18,7 +17,6 @@ namespace MediaBrowser.Api.Library
     /// Class GetDefaultVirtualFolders
     /// </summary>
     [Route("/Library/VirtualFolders", "GET")]
-    [Route("/Users/{UserId}/VirtualFolders", "GET")]
     public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
     {
         /// <summary>
@@ -143,11 +141,6 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         private readonly IServerApplicationPaths _appPaths;
 
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
         /// <summary>
         /// The _library manager
         /// </summary>
@@ -156,27 +149,21 @@ namespace MediaBrowser.Api.Library
         private readonly ILibraryMonitor _libraryMonitor;
 
         private readonly IFileSystem _fileSystem;
-        private readonly ILogger _logger;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryStructureService"/> class.
+        /// Initializes a new instance of the <see cref="LibraryStructureService" /> class.
         /// </summary>
-        /// <param name="appPaths">The app paths.</param>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IFileSystem fileSystem, ILogger logger)
+        public LibraryStructureService(IServerApplicationPaths appPaths, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IFileSystem fileSystem)
         {
             if (appPaths == null)
             {
                 throw new ArgumentNullException("appPaths");
             }
 
-            _userManager = userManager;
             _appPaths = appPaths;
             _libraryManager = libraryManager;
             _libraryMonitor = libraryMonitor;
             _fileSystem = fileSystem;
-            _logger = logger;
         }
 
         /// <summary>
@@ -186,20 +173,9 @@ namespace MediaBrowser.Api.Library
         /// <returns>System.Object.</returns>
         public object Get(GetVirtualFolders request)
         {
-            if (string.IsNullOrEmpty(request.UserId))
-            {
-                var result = _libraryManager.GetDefaultVirtualFolders().OrderBy(i => i.Name).ToList();
+            var result = _libraryManager.GetVirtualFolders().OrderBy(i => i.Name).ToList();
 
-                return ToOptimizedSerializedResultUsingCache(result);
-            }
-            else
-            {
-                var user = _userManager.GetUserById(request.UserId);
-
-                var result = _libraryManager.GetVirtualFolders(user).OrderBy(i => i.Name).ToList();
-
-                return ToOptimizedSerializedResultUsingCache(result);
-            }
+            return ToOptimizedSerializedResultUsingCache(result);
         }
 
         /// <summary>
@@ -348,7 +324,7 @@ namespace MediaBrowser.Api.Library
 
             try
             {
-                Directory.Delete(path, true);
+                _fileSystem.DeleteDirectory(path, true);
 
                 // Need to add a delay here or directory watchers may still pick up the changes
                 var delayTask = Task.Delay(1000);

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

@@ -82,6 +82,10 @@
     <Compile Include="Playback\Hls\MpegDashService.cs" />
     <Compile Include="Playback\MediaInfoService.cs" />
     <Compile Include="PlaylistService.cs" />
+    <Compile Include="Reports\ReportFieldType.cs" />
+    <Compile Include="Reports\ReportResult.cs" />
+    <Compile Include="Reports\ReportsService.cs" />
+    <Compile Include="Reports\ReportRequests.cs" />
     <Compile Include="StartupWizardService.cs" />
     <Compile Include="Subtitles\SubtitleService.cs" />
     <Compile Include="Movies\CollectionService.cs" />
@@ -110,7 +114,6 @@
     <Compile Include="NotificationsService.cs" />
     <Compile Include="PackageReviewService.cs" />
     <Compile Include="PackageService.cs" />
-    <Compile Include="Playback\BifService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
     <Compile Include="Playback\Hls\DynamicHlsService.cs" />
     <Compile Include="Playback\Hls\HlsSegmentService.cs" />
@@ -131,6 +134,8 @@
     <Compile Include="SearchService.cs" />
     <Compile Include="Session\SessionsService.cs" />
     <Compile Include="SimilarItemsHelper.cs" />
+    <Compile Include="Sync\SyncJobWebSocketListener.cs" />
+    <Compile Include="Sync\SyncJobsWebSocketListener.cs" />
     <Compile Include="Sync\SyncService.cs" />
     <Compile Include="System\ActivityLogService.cs" />
     <Compile Include="System\ActivityLogWebSocketListener.cs" />

+ 1 - 1
MediaBrowser.Api/Movies/CollectionService.cs

@@ -71,7 +71,7 @@ namespace MediaBrowser.Api.Movies
 
             }).ConfigureAwait(false);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
 

+ 12 - 13
MediaBrowser.Api/Movies/MoviesService.cs

@@ -121,8 +121,7 @@ namespace MediaBrowser.Api.Movies
         {
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var movies = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId)
-                .Where(i => i is Movie);
+            IEnumerable<BaseItem> movies = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, i => i is Movie);
 
             movies = _libraryManager.ReplaceVideosWithPrimaryVersions(movies);
 
@@ -157,7 +156,7 @@ namespace MediaBrowser.Api.Movies
                 .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase)
                 .ToList();
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             dtoOptions.Fields = request.GetItemFields().ToList();
             
@@ -174,13 +173,11 @@ namespace MediaBrowser.Api.Movies
                 (request.UserId.HasValue ? user.RootFolder :
                 _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
 
-            var fields = request.GetItemFields().ToList();
-
+            Func<BaseItem, bool> filter = i => i.Id != item.Id && includeInSearch(i);
+            
             var inputItems = user == null
-                                 ? _libraryManager.RootFolder.GetRecursiveChildren().Where(i => i.Id != item.Id)
-                                 : user.RootFolder.GetRecursiveChildren(user).Where(i => i.Id != item.Id);
-
-            inputItems = inputItems.Where(includeInSearch);
+                                 ? _libraryManager.RootFolder.GetRecursiveChildren(filter)
+                                 : user.RootFolder.GetRecursiveChildren(user, filter);
 
             var list = inputItems.ToList();
 
@@ -225,10 +222,12 @@ namespace MediaBrowser.Api.Movies
             {
                 returnItems = returnItems.Take(request.Limit.Value);
             }
+
+            var dtoOptions = GetDtoOptions(request);
           
             var result = new ItemsResult
             {
-                Items = returnItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray(),
+                Items = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ToArray(),
 
                 TotalRecordCount = items.Count
             };
@@ -351,7 +350,7 @@ namespace MediaBrowser.Api.Movies
                         BaselineItemName = director,
                         CategoryId = director.GetMD5().ToString("N"),
                         RecommendationType = type,
-                        Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray()
+                        Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray()
                     };
                 }
             }
@@ -375,7 +374,7 @@ namespace MediaBrowser.Api.Movies
                         BaselineItemName = name,
                         CategoryId = name.GetMD5().ToString("N"),
                         RecommendationType = type,
-                        Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray()
+                        Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray()
                     };
                 }
             }
@@ -399,7 +398,7 @@ namespace MediaBrowser.Api.Movies
                         BaselineItemName = item.Name,
                         CategoryId = item.Id.ToString("N"),
                         RecommendationType = type,
-                        Items = similar.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray()
+                        Items = _dtoService.GetBaseItemDtos(similar, dtoOptions, user).ToArray()
                     };
                 }
             }

+ 5 - 3
MediaBrowser.Api/Movies/TrailersService.cs

@@ -84,7 +84,9 @@ namespace MediaBrowser.Api.Movies
         /// <returns>System.Object.</returns>
         public object Get(GetSimilarTrailers request)
         {
-            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
+            var dtoOptions = GetDtoOptions(request);
+
+            var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager,
                 _itemRepo,
                 _libraryManager,
                 _userDataRepository,
@@ -119,9 +121,9 @@ namespace MediaBrowser.Api.Movies
 
             var pagedItems = ApplyPaging(request, itemsArray);
 
-            var fields = request.GetItemFields().ToList();
+            var dtoOptions = GetDtoOptions(request);
 
-            var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray();
+            var returnItems = _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user).ToArray();
 
             return new ItemsResult
             {

+ 7 - 5
MediaBrowser.Api/Music/AlbumsService.cs

@@ -50,7 +50,9 @@ namespace MediaBrowser.Api.Music
         /// <returns>System.Object.</returns>
         public object Get(GetSimilarAlbums request)
         {
-            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
+            var dtoOptions = GetDtoOptions(request);
+
+            var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager,
                 _itemRepo,
                 _libraryManager,
                 _userDataRepository,
@@ -75,14 +77,14 @@ namespace MediaBrowser.Api.Music
             var album1 = (MusicAlbum)item1;
             var album2 = (MusicAlbum)item2;
 
-            var artists1 = album1.GetRecursiveChildren()
-                .OfType<Audio>()
+            var artists1 = album1.GetRecursiveChildren(i => i is IHasArtist)
+                .Cast<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToList();
 
-            var artists2 = album2.GetRecursiveChildren()
-                .OfType<Audio>()
+            var artists2 = album2.GetRecursiveChildren(i => i is IHasArtist)
+                .Cast<IHasArtist>()
                 .SelectMany(i => i.AllArtists)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);

+ 11 - 14
MediaBrowser.Api/Music/InstantMixService.cs

@@ -73,44 +73,44 @@ namespace MediaBrowser.Api.Music
 
         public object Get(GetInstantMixFromArtistId request)
         {
-            var item = (MusicArtist)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromArtist(item.Name, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromMusicGenreId request)
         {
-            var item = (MusicGenre)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromGenres(new[] { item.Name }, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromSong request)
         {
-            var item = (Audio)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromSong(item, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromAlbum request)
         {
-            var album = (MusicAlbum)_libraryManager.GetItemById(request.Id);
+            var album = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromAlbum(album, user);
+            var items = _musicManager.GetInstantMixFromItem(album, user);
 
             return GetResult(items, user, request);
         }
@@ -121,7 +121,7 @@ namespace MediaBrowser.Api.Music
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromPlaylist(playlist, user);
+            var items = _musicManager.GetInstantMixFromItem(playlist, user);
 
             return GetResult(items, user, request);
         }
@@ -146,8 +146,6 @@ namespace MediaBrowser.Api.Music
 
         private object GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request)
         {
-            var fields = request.GetItemFields().ToList();
-
             var list = items.ToList();
 
             var result = new ItemsResult
@@ -155,10 +153,9 @@ namespace MediaBrowser.Api.Music
                 TotalRecordCount = list.Count
             };
 
-            var dtos = list.Take(request.Limit ?? list.Count)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user));
+            var dtoOptions = GetDtoOptions(request);
 
-            result.Items = dtos.ToArray();
+            result.Items = _dtoService.GetBaseItemDtos(list.Take(request.Limit ?? list.Count), dtoOptions, user).ToArray();
 
             return ToOptimizedResult(result);
         }

+ 130 - 14
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
@@ -66,14 +67,16 @@ namespace MediaBrowser.Api.Playback
 
         protected ILiveTvManager LiveTvManager { get; private set; }
         protected IDlnaManager DlnaManager { get; private set; }
+        protected IDeviceManager DeviceManager { get; private set; }
         protected IChannelManager ChannelManager { get; private set; }
         protected ISubtitleEncoder SubtitleEncoder { get; private set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
         /// </summary>
-        protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
+        protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager)
         {
+            DeviceManager = deviceManager;
             SubtitleEncoder = subtitleEncoder;
             ChannelManager = channelManager;
             DlnaManager = dlnaManager;
@@ -119,8 +122,8 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         private string GetOutputFilePath(StreamState state)
         {
-            var folder = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, EncodingContext.Streaming.ToString().ToLower());
-            
+            var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
+
             var outputFileExtension = GetOutputFileExtension(state);
 
             var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false);
@@ -320,13 +323,13 @@ namespace MediaBrowser.Api.Playback
                 switch (qualitySetting)
                 {
                     case EncodingQuality.HighSpeed:
-                        param += " -crf 23";
+                        param += " -subq 0 -crf 23";
                         break;
                     case EncodingQuality.HighQuality:
-                        param += " -crf 20";
+                        param += " -subq 3 -crf 20";
                         break;
                     case EncodingQuality.MaxQuality:
-                        param += " -crf 18";
+                        param += " -subq 6 -crf 18";
                         break;
                 }
             }
@@ -349,6 +352,41 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
+            // h264 (h264_qsv)
+            else if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (qualitySetting)
+                {
+                    case EncodingQuality.HighSpeed:
+                        param = "-preset 7";
+                        break;
+                    case EncodingQuality.HighQuality:
+                        param = "-preset 4";
+                        break;
+                    case EncodingQuality.MaxQuality:
+                        param = "-preset 1";
+                        break;
+                }
+
+            }
+
+            // h264 (libnvenc)
+            else if (string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (qualitySetting)
+                {
+                    case EncodingQuality.HighSpeed:
+                        param = "-preset high-performance";
+                        break;
+                    case EncodingQuality.HighQuality:
+                        param = "";
+                        break;
+                    case EncodingQuality.MaxQuality:
+                        param = "-preset high-quality";
+                        break;
+                }
+            }
+
             // webm
             else if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
             {
@@ -426,10 +464,50 @@ namespace MediaBrowser.Api.Playback
 
             if (!string.IsNullOrEmpty(state.VideoRequest.Level))
             {
-                param += " -level " + state.VideoRequest.Level;
+                // h264_qsv and libnvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
+                if (String.Equals(H264Encoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || String.Equals(H264Encoder, "libnvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    switch (state.VideoRequest.Level)
+                    {
+                        case "30":
+                            param += " -level 3";
+                            break;
+                        case "31":
+                            param += " -level 3.1";
+                            break;
+                        case "32":
+                            param += " -level 3.2";
+                            break;
+                        case "40":
+                            param += " -level 4";
+                            break;
+                        case "41":
+                            param += " -level 4.1";
+                            break;
+                        case "42":
+                            param += " -level 4.2";
+                            break;
+                        case "50":
+                            param += " -level 5";
+                            break;
+                        case "51":
+                            param += " -level 5.1";
+                            break;
+                        case "52":
+                            param += " -level 5.2";
+                            break;
+                        default:
+                            param += " -level " + state.VideoRequest.Level;
+                            break;
+                    }
+                }
+                else
+                {
+                    param += " -level " + state.VideoRequest.Level;
+                }
             }
 
-            return param;
+            return "-pix_fmt yuv420p " + param;
         }
 
         protected string GetAudioFilterParam(StreamState state, bool isHls)
@@ -567,6 +645,11 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
+            if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            {
+                filters[filters.Count - 1] += ":flags=fast_bilinear";
+            }
+
             var output = string.Empty;
 
             if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
@@ -606,7 +689,7 @@ namespace MediaBrowser.Api.Playback
 
                 if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
                 {
-                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language);
+                    var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath);
 
                     if (!string.IsNullOrEmpty(charenc))
                     {
@@ -834,7 +917,7 @@ namespace MediaBrowser.Api.Playback
                 {
                     if (SupportsThrottleWithStream)
                     {
-                        var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
+                        var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
 
                         url += "&transcodingJobId=" + transcodingJobId;
 
@@ -1183,6 +1266,22 @@ namespace MediaBrowser.Api.Playback
                     return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
                 }
 
+                // h264_qsv
+                if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    if (hasFixedResolution)
+                    {
+                        if (isHls)
+                        {
+                            return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
+                        }
+
+                        return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
+                    }
+
+                    return string.Format(" -b:v {0} -maxrate ({0}*1.2) -bufsize ({0}*2)", bitrate.Value.ToString(UsCulture));
+                }
+
                 // H264
                 if (hasFixedResolution)
                 {
@@ -1991,9 +2090,26 @@ namespace MediaBrowser.Api.Playback
                 headers[key] = Request.Headers[key];
             }
 
-            state.DeviceProfile = string.IsNullOrWhiteSpace(state.Request.DeviceProfileId) ?
-                DlnaManager.GetProfile(headers) :
-                DlnaManager.GetProfile(state.Request.DeviceProfileId);
+            if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
+            {
+                state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
+            }
+            else
+            {
+                if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
+                {
+                    var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
+
+                    if (caps != null)
+                    {
+                        state.DeviceProfile = caps.DeviceProfile;
+                    }
+                    else
+                    {
+                        state.DeviceProfile = DlnaManager.GetProfile(headers);
+                    }
+                }
+            }
 
             var profile = state.DeviceProfile;
 

+ 0 - 186
MediaBrowser.Api/Playback/BifService.cs

@@ -1,186 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using ServiceStack;
-using System;
-using System.Collections.Concurrent;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.Playback
-{
-    [Route("/Videos/{Id}/index.bif", "GET")]
-    public class GetBifFile
-    {
-        [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxWidth { get; set; }
-
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    public class BifService : BaseApiService
-    {
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IFileSystem _fileSystem;
-
-        public BifService(IServerApplicationPaths appPaths, ILibraryManager libraryManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem)
-        {
-            _appPaths = appPaths;
-            _libraryManager = libraryManager;
-            _mediaEncoder = mediaEncoder;
-            _fileSystem = fileSystem;
-        }
-
-        public object Get(GetBifFile request)
-        {
-            return ToStaticFileResult(GetBifFile(request).Result);
-        }
-
-        private async Task<string> GetBifFile(GetBifFile request)
-        {
-            var widthVal = request.MaxWidth.HasValue ? request.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty;
-
-            var item = _libraryManager.GetItemById(request.Id);
-            var mediaSources = ((IHasMediaSources)item).GetMediaSources(false).ToList();
-            var mediaSource = mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)) ?? mediaSources.First();
-
-            var path = Path.Combine(_appPaths.ImageCachePath, "bif", request.Id, request.MediaSourceId, widthVal, "index.bif");
-
-            if (File.Exists(path))
-            {
-                return path;
-            }
-
-            var protocol = mediaSource.Protocol;
-
-            var inputPath = MediaEncoderHelpers.GetInputArgument(mediaSource.Path, protocol, null, mediaSource.PlayableStreamFileNames);
-
-            var semaphore = GetLock(path);
-
-            await semaphore.WaitAsync().ConfigureAwait(false);
-
-            try
-            {
-                if (File.Exists(path))
-                {
-                    return path;
-                }
-                
-                await _mediaEncoder.ExtractVideoImagesOnInterval(inputPath, protocol, mediaSource.Video3DFormat,
-                        TimeSpan.FromSeconds(10), Path.GetDirectoryName(path), "img_", request.MaxWidth, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                var images = new DirectoryInfo(Path.GetDirectoryName(path))
-                    .EnumerateFiles()
-                    .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal))
-                    .OrderBy(i => i.FullName)
-                    .ToList();
-
-                using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true))
-                {
-                    var magicNumber = new byte[] { 0x89, 0x42, 0x49, 0x46, 0x0d, 0x0a, 0x1a, 0x0a };
-                    await fs.WriteAsync(magicNumber, 0, magicNumber.Length);
-
-                    // version
-                    var bytes = GetBytes(0);
-                    await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                    // image count
-                    bytes = GetBytes(images.Count);
-                    await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                    // interval in ms
-                    bytes = GetBytes(10000);
-                    await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                    // reserved
-                    for (var i = 20; i <= 63; i++)
-                    {
-                        bytes = new byte[] { 0x00 };
-                        await fs.WriteAsync(bytes, 0, bytes.Length);
-                    }
-
-                    // write the bif index
-                    var index = 0;
-                    long imageOffset = 64 + (8 * images.Count) + 8;
-
-                    foreach (var img in images)
-                    {
-                        bytes = GetBytes(index);
-                        await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                        bytes = GetBytes(imageOffset);
-                        await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                        imageOffset += img.Length;
-
-                        index++;
-                    }
-
-                    bytes = new byte[] { 0xff, 0xff, 0xff, 0xff };
-                    await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                    bytes = GetBytes(imageOffset);
-                    await fs.WriteAsync(bytes, 0, bytes.Length);
-
-                    // write the images
-                    foreach (var img in images)
-                    {
-                        using (var imgStream = _fileSystem.GetFileStream(img.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-                        {
-                            await imgStream.CopyToAsync(fs).ConfigureAwait(false);
-                        }
-                    }
-                }
-
-                return path;
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
-        private byte[] GetBytes(int value)
-        {
-            byte[] bytes = BitConverter.GetBytes(value);
-            if (!BitConverter.IsLittleEndian)
-                Array.Reverse(bytes);
-            return bytes;
-        }
-
-        private byte[] GetBytes(long value)
-        {
-            var intVal = Convert.ToInt32(value);
-            return GetBytes(intVal);
-
-            //byte[] bytes = BitConverter.GetBytes(value);
-            //if (BitConverter.IsLittleEndian)
-            //    Array.Reverse(bytes);
-            //return bytes;
-        }
-
-        private static readonly ConcurrentDictionary<string, SemaphoreSlim> SemaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
-
-        /// <summary>
-        /// Gets the lock.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.Object.</returns>
-        private static SemaphoreSlim GetLock(string filename)
-        {
-            return SemaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
-        }
-    }
-}

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

@@ -1,20 +1,20 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -23,8 +23,7 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     public abstract class BaseHlsService : BaseStreamingService
     {
-        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
-            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
         {
         }
 

+ 15 - 21
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
@@ -62,38 +63,31 @@ namespace MediaBrowser.Api.Playback.Hls
 
     public class DynamicHlsService : BaseHlsService
     {
-        protected INetworkManager NetworkManager { get; private set; }
-
-        public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, INetworkManager networkManager)
-            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
         {
             NetworkManager = networkManager;
         }
 
-        public object Get(GetMasterHlsVideoStream request)
-        {
-            var result = GetAsync(request, "GET").Result;
+        protected INetworkManager NetworkManager { get; private set; }
 
-            return result;
+        public Task<object> Get(GetMasterHlsVideoStream request)
+        {
+            return GetAsync(request, "GET");
         }
 
-        public object Head(GetMasterHlsVideoStream request)
+        public Task<object> Head(GetMasterHlsVideoStream request)
         {
-            var result = GetAsync(request, "HEAD").Result;
-
-            return result;
+            return GetAsync(request, "HEAD");
         }
 
-        public object Get(GetMainHlsVideoStream request)
+        public Task<object> Get(GetMainHlsVideoStream request)
         {
-            var result = GetPlaylistAsync(request, "main").Result;
-
-            return result;
+            return GetPlaylistAsync(request, "main");
         }
 
-        public object Get(GetDynamicHlsVideoSegment request)
+        public Task<object> Get(GetDynamicHlsVideoSegment request)
         {
-            return GetDynamicSegment(request, request.SegmentId).Result;
+            return GetDynamicSegment(request, request.SegmentId);
         }
 
         private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId)
@@ -210,10 +204,10 @@ namespace MediaBrowser.Api.Playback.Hls
             {
                 return;
             }
-
+            
             try
             {
-                File.Delete(file.FullName);
+                FileSystem.DeleteFile(file.FullName);
             }
             catch (IOException ex)
             {

+ 2 - 6
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -1,10 +1,6 @@
 using MediaBrowser.Controller;
-using MediaBrowser.Model.Dlna;
 using ServiceStack;
-using System;
 using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -66,7 +62,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
 
-            file = Path.Combine(_appPaths.TranscodingTempPath, EncodingContext.Streaming.ToString().ToLower(), file);
+            file = Path.Combine(_appPaths.TranscodingTempPath, file);
 
             return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
         }
@@ -85,7 +81,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
 
-            file = Path.Combine(_appPaths.TranscodingTempPath, EncodingContext.Streaming.ToString().ToLower(), file);
+            file = Path.Combine(_appPaths.TranscodingTempPath, file);
 
             return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
         }

+ 6 - 6
MediaBrowser.Api/Playback/Hls/MpegDashService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
@@ -50,14 +51,13 @@ namespace MediaBrowser.Api.Playback.Hls
 
     public class MpegDashService : BaseHlsService
     {
-        protected INetworkManager NetworkManager { get; private set; }
-
-        public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, INetworkManager networkManager)
-            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
         {
             NetworkManager = networkManager;
         }
 
+        protected INetworkManager NetworkManager { get; private set; }
+
         public object Get(GetMasterManifest request)
         {
             var result = GetAsync(request, "GET").Result;
@@ -489,7 +489,7 @@ namespace MediaBrowser.Api.Playback.Hls
             {
                 try
                 {
-                    File.Delete(file.FullName);
+                    FileSystem.DeleteFile(file.FullName);
                 }
                 catch (IOException ex)
                 {
@@ -625,7 +625,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
         {
-            // test url http://192.168.1.2:8096/mediabrowser/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
+            // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
             // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
 
             var threads = GetNumberOfThreads(state, false);

+ 3 - 4
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -1,11 +1,11 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
@@ -57,8 +57,7 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     public class VideoHlsService : BaseHlsService
     {
-        public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
-            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
         {
         }
 
@@ -71,7 +70,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
 
-            file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, EncodingContext.Streaming.ToString().ToLower(), file);
+            file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file);
 
             return ResultFactory.GetStaticFileResult(Request, file);
         }

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

@@ -2,6 +2,7 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
@@ -31,7 +32,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     /// </summary>
     public class AudioService : BaseProgressiveStreamingService
     {
-        public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, imageProcessor, httpClient)
+        public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
         {
         }
 

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

@@ -1,8 +1,8 @@
-using System.Linq;
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
@@ -15,6 +15,7 @@ using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -28,8 +29,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         protected readonly IImageProcessor ImageProcessor;
         protected readonly IHttpClient HttpClient;
 
-        protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient)
-            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
         {
             ImageProcessor = imageProcessor;
             HttpClient = httpClient;

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

@@ -2,6 +2,7 @@ using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     /// </summary>
     public class VideoService : BaseProgressiveStreamingService
     {
-        public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, imageProcessor, httpClient)
+        public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
         {
         }
 
@@ -96,6 +97,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase))
             {
+                // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
                 format = " -f mp4 -movflags frag_keyframe+empty_moov";
             }
 

+ 4 - 1
MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs

@@ -40,7 +40,10 @@ namespace MediaBrowser.Api.Playback
         /// <param name="responseStream">The response stream.</param>
         public void WriteTo(Stream responseStream)
         {
-            _response.Content.CopyTo(responseStream, 819200);
+            using (_response)
+            {
+                _response.Content.CopyTo(responseStream, 819200);
+            }
         }
     }
 }

+ 4 - 3
MediaBrowser.Api/PlaylistService.cs

@@ -151,9 +151,10 @@ namespace MediaBrowser.Api
             {
                 items = items.Take(request.Limit.Value).ToArray();
             }
-            
-            var dtos = items
-                   .Select(i => _dtoService.GetBaseItemDto(i.Item2, request.GetItemFields().ToList(), user))
+
+            var dtoOptions = GetDtoOptions(request);
+
+            var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2), dtoOptions, user)
                    .ToArray();
 
             var index = 0;

+ 3 - 5
MediaBrowser.Api/PluginService.cs

@@ -1,5 +1,4 @@
-using System.Threading;
-using MediaBrowser.Common;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Security;
 using MediaBrowser.Common.Updates;
@@ -8,12 +7,12 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Serialization;
 using ServiceStack;
-using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Api
@@ -236,8 +235,7 @@ namespace MediaBrowser.Api
         {
             // We need to parse this manually because we told service stack not to with IRequiresRequestStream
             // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+            var id = new Guid(GetPathValue(1));
 
             var plugin = _appHost.Plugins.First(p => p.Id == id);
 

+ 9 - 0
MediaBrowser.Api/Reports/ReportFieldType.cs

@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Api.Reports
+{
+    public enum ReportFieldType
+    {
+        String,
+        Boolean
+    }
+}

+ 33 - 0
MediaBrowser.Api/Reports/ReportRequests.cs

@@ -0,0 +1,33 @@
+using ServiceStack;
+
+namespace MediaBrowser.Api.Reports
+{
+    public class BaseReportRequest : IReturn<ReportResult>
+    {
+        /// <summary>
+        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
+        /// </summary>
+        /// <value>The parent id.</value>
+        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ParentId { get; set; }
+
+        /// <summary>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Limit { get; set; }
+    }
+
+    [Route("/Reports/Items", "GET", Summary = "Gets reports based on library items")]
+    public class GetItemReport : BaseReportRequest
+    {
+    }
+}

+ 16 - 0
MediaBrowser.Api/Reports/ReportResult.cs

@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Api.Reports
+{
+    public class ReportResult
+    {
+        public List<List<string>> Rows { get; set; }
+        public List<ReportFieldType> Columns { get; set; }
+
+        public ReportResult()
+        {
+            Rows = new List<List<string>>();
+            Columns = new List<ReportFieldType>();
+        }
+    }
+}

+ 64 - 0
MediaBrowser.Api/Reports/ReportsService.cs

@@ -0,0 +1,64 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Querying;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Reports
+{
+    public class ReportsService : BaseApiService
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public ReportsService(ILibraryManager libraryManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        public async Task<object> Get(GetItemReport request)
+        {
+            var queryResult = await GetQueryResult(request).ConfigureAwait(false);
+
+            var reportResult = GetReportResult(queryResult);
+
+            return ToOptimizedResult(reportResult);
+        }
+
+        private ReportResult GetReportResult(QueryResult<BaseItem> queryResult)
+        {
+            var reportResult = new ReportResult();
+
+            // Fill rows and columns
+
+            return reportResult;
+        }
+
+        private Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request)
+        {
+            // Placeholder in case needed later
+            User user = null;
+
+            var parentItem = string.IsNullOrEmpty(request.ParentId) ?
+                (user == null ? _libraryManager.RootFolder : user.RootFolder) :
+                _libraryManager.GetItemById(request.ParentId);
+
+            return ((Folder)parentItem).GetItems(GetItemsQuery(request, user));
+        }
+
+        private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user)
+        {
+            var query = new InternalItemsQuery
+            {
+                User = user,
+                CollapseBoxSetItems = false
+            };
+
+            // Set query values based on request
+
+            // Example
+            //query.IncludeItemTypes = new[] {"Movie"};
+
+
+            return query;
+        }
+    }
+}

+ 23 - 2
MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs

@@ -32,6 +32,9 @@ namespace MediaBrowser.Api.ScheduledTasks
     {
         [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; }
+
+        [ApiMember(Name = "IsEnabled", Description = "Optional filter tasks that are enabled, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsEnabled { get; set; }
     }
 
     /// <summary>
@@ -132,6 +135,25 @@ namespace MediaBrowser.Api.ScheduledTasks
                 });
             }
 
+            if (request.IsEnabled.HasValue)
+            {
+                var val = request.IsEnabled.Value;
+
+                result = result.Where(i =>
+                {
+                    var isEnabled = true;
+
+                    var configurableTask = i.ScheduledTask as IConfigurableScheduledTask;
+
+                    if (configurableTask != null)
+                    {
+                        isEnabled = configurableTask.IsEnabled;
+                    }
+
+                    return isEnabled == val;
+                });
+            }
+            
             var infos = result
                 .Select(ScheduledTaskHelpers.GetTaskInfo)
                 .ToList();
@@ -202,8 +224,7 @@ namespace MediaBrowser.Api.ScheduledTasks
         {
             // We need to parse this manually because we told service stack not to with IRequiresRequestStream
             // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = pathInfo.GetArgumentValue<string>(1);
+            var id = GetPathValue(1);
 
             var task = TaskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, id));
 

+ 5 - 10
MediaBrowser.Api/SearchService.cs

@@ -194,29 +194,24 @@ namespace MediaBrowser.Api
             {
                 result.Series = season.Series.Name;
 
-                result.EpisodeCount = season.GetRecursiveChildren().Count(i => i is Episode);
+                result.EpisodeCount = season.GetRecursiveChildren(i => i is Episode).Count;
             }
 
             var series = item as Series;
 
             if (series != null)
             {
-                result.EpisodeCount = series.GetRecursiveChildren().Count(i => i is Episode);
+                result.EpisodeCount = series.GetRecursiveChildren(i => i is Episode).Count;
             }
 
             var album = item as MusicAlbum;
 
             if (album != null)
             {
-                var songs = album.GetRecursiveChildren().OfType<Audio>().ToList();
+                result.SongCount = album.Tracks.Count();
 
-                result.SongCount = songs.Count;
-
-                result.Artists = songs.SelectMany(i => i.AllArtists)
-                    .Distinct(StringComparer.OrdinalIgnoreCase)
-                    .ToArray();
-
-                result.AlbumArtist = songs.SelectMany(i => i.AlbumArtists).FirstOrDefault(i => !string.IsNullOrEmpty(i));
+                result.Artists = album.Artists.ToArray();
+                result.AlbumArtist = album.AlbumArtists.FirstOrDefault();
             }
 
             var song = item as Audio;

+ 1 - 2
MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;

+ 8 - 3
MediaBrowser.Api/Session/SessionsService.cs

@@ -243,8 +243,13 @@ namespace MediaBrowser.Api.Session
         [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
         public bool SupportsSync { get; set; }
 
-        [ApiMember(Name = "SupportsUniqueIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsUniqueIdentifier { get; set; }
+        [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
+        public bool SupportsPersistentIdentifier { get; set; }
+
+        public PostCapabilities()
+        {
+            SupportsPersistentIdentifier = true;
+        }
     }
 
     [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")]
@@ -556,7 +561,7 @@ namespace MediaBrowser.Api.Session
 
                 SupportsSync = request.SupportsSync,
 
-                SupportsUniqueIdentifier = request.SupportsUniqueIdentifier
+                SupportsPersistentIdentifier = request.SupportsPersistentIdentifier
             });
         }
 

+ 8 - 7
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -57,6 +57,7 @@ namespace MediaBrowser.Api
         /// <summary>
         /// Gets the similar items.
         /// </summary>
+        /// <param name="dtoOptions">The dto options.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="libraryManager">The library manager.</param>
@@ -67,7 +68,7 @@ namespace MediaBrowser.Api
         /// <param name="includeInSearch">The include in search.</param>
         /// <param name="getSimilarityScore">The get similarity score.</param>
         /// <returns>ItemsResult.</returns>
-        internal static ItemsResult GetSimilarItemsResult(IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, ILogger logger, BaseGetSimilarItemsFromItem request, Func<BaseItem, bool> includeInSearch, Func<BaseItem, BaseItem, int> getSimilarityScore)
+        internal static ItemsResult GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, ILogger logger, BaseGetSimilarItemsFromItem request, Func<BaseItem, bool> includeInSearch, Func<BaseItem, BaseItem, int> getSimilarityScore)
         {
             var user = request.UserId.HasValue ? userManager.GetUserById(request.UserId.Value) : null;
 
@@ -75,13 +76,13 @@ namespace MediaBrowser.Api
                 (request.UserId.HasValue ? user.RootFolder :
                 libraryManager.RootFolder) : libraryManager.GetItemById(request.Id);
 
-            var fields = request.GetItemFields().ToList();
+            Func<BaseItem, bool> filter = i => i.Id != item.Id && includeInSearch(i);
 
             var inputItems = user == null
-                                 ? libraryManager.RootFolder.GetRecursiveChildren().Where(i => i.Id != item.Id)
-                                 : user.RootFolder.GetRecursiveChildren(user).Where(i => i.Id != item.Id);
+                                 ? libraryManager.RootFolder.GetRecursiveChildren(filter)
+                                 : user.RootFolder.GetRecursiveChildren(user, filter);
 
-            var items = GetSimilaritems(item, inputItems.Where(includeInSearch), getSimilarityScore)
+            var items = GetSimilaritems(item, inputItems, getSimilarityScore)
                 .ToList();
 
             IEnumerable<BaseItem> returnItems = items;
@@ -93,7 +94,7 @@ namespace MediaBrowser.Api
 
             var result = new ItemsResult
             {
-                Items = returnItems.Select(i => dtoService.GetBaseItemDto(i, fields, user)).ToArray(),
+                Items = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ToArray(),
 
                 TotalRecordCount = items.Count
             };
@@ -164,7 +165,7 @@ namespace MediaBrowser.Api
 
             // Find common keywords
             points += GetKeywords(item1).Where(i => GetKeywords(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-            
+
             // Find common studios
             points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
 

+ 3 - 0
MediaBrowser.Api/StartupWizardService.cs

@@ -62,6 +62,9 @@ namespace MediaBrowser.Api
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.Configuration.EnableLocalizedGuids = true;
+            _config.Configuration.MergeMetadataAndImagesByName = true;
+            _config.Configuration.EnableStandaloneMetadata = true;
+            _config.Configuration.EnableLibraryMetadataSubFolder = true;
             _config.SaveConfiguration();
         }
 

+ 120 - 0
MediaBrowser.Api/Sync/SyncJobWebSocketListener.cs

@@ -0,0 +1,120 @@
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Sync;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Sync;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Sync
+{
+    /// <summary>
+    /// Class SessionInfoWebSocketListener
+    /// </summary>
+    class SyncJobWebSocketListener : BasePeriodicWebSocketListener<CompleteSyncJobInfo, WebSocketListenerState>
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        protected override string Name
+        {
+            get { return "SyncJob"; }
+        }
+
+        private readonly ISyncManager _syncManager;
+        private string _jobId;
+
+        public SyncJobWebSocketListener(ILogger logger, ISyncManager syncManager)
+            : base(logger)
+        {
+            _syncManager = syncManager;
+            _syncManager.SyncJobCancelled += _syncManager_SyncJobCancelled;
+            _syncManager.SyncJobUpdated += _syncManager_SyncJobUpdated;
+            _syncManager.SyncJobItemCreated += _syncManager_SyncJobItemCreated;
+            _syncManager.SyncJobItemUpdated += _syncManager_SyncJobItemUpdated;
+        }
+
+        void _syncManager_SyncJobItemUpdated(object sender, GenericEventArgs<SyncJobItem> e)
+        {
+            if (string.Equals(e.Argument.Id, _jobId, StringComparison.Ordinal))
+            {
+                SendData(false);
+            }
+        }
+
+        void _syncManager_SyncJobItemCreated(object sender, GenericEventArgs<SyncJobItem> e)
+        {
+            if (string.Equals(e.Argument.Id, _jobId, StringComparison.Ordinal))
+            {
+                SendData(true);
+            }
+        }
+
+        protected override void ParseMessageParams(string[] values)
+        {
+            base.ParseMessageParams(values);
+
+            if (values.Length > 0)
+            {
+                _jobId = values[0];
+            }
+        }
+
+        void _syncManager_SyncJobUpdated(object sender, GenericEventArgs<SyncJob> e)
+        {
+            if (string.Equals(e.Argument.Id, _jobId, StringComparison.Ordinal))
+            {
+                SendData(false);
+            }
+        }
+
+        void _syncManager_SyncJobCancelled(object sender, GenericEventArgs<SyncJob> e)
+        {
+            if (string.Equals(e.Argument.Id, _jobId, StringComparison.Ordinal))
+            {
+                SendData(true);
+            }
+        }
+
+        /// <summary>
+        /// Gets the data to send.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>Task{SystemInfo}.</returns>
+        protected override Task<CompleteSyncJobInfo> GetDataToSend(WebSocketListenerState state)
+        {
+            var job = _syncManager.GetJob(_jobId);
+            var items = _syncManager.GetJobItems(new SyncJobItemQuery
+            {
+                AddMetadata = true,
+                JobId = _jobId
+            });
+
+            var info = new CompleteSyncJobInfo
+            {
+                Job = job,
+                JobItems = items.Items.ToList()
+            };
+
+            return Task.FromResult(info);
+        }
+
+        protected override bool SendOnTimer
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        protected override void Dispose(bool dispose)
+        {
+            _syncManager.SyncJobCancelled -= _syncManager_SyncJobCancelled;
+            _syncManager.SyncJobUpdated -= _syncManager_SyncJobUpdated;
+
+            base.Dispose(dispose);
+        }
+    }
+}

+ 101 - 0
MediaBrowser.Api/Sync/SyncJobsWebSocketListener.cs

@@ -0,0 +1,101 @@
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Sync;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Sync;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Sync
+{
+    /// <summary>
+    /// Class SessionInfoWebSocketListener
+    /// </summary>
+    class SyncJobsWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SyncJob>, WebSocketListenerState>
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        protected override string Name
+        {
+            get { return "SyncJobs"; }
+        }
+
+        private readonly ISyncManager _syncManager;
+        private string _userId;
+        private string _targetId;
+
+        public SyncJobsWebSocketListener(ILogger logger, ISyncManager syncManager)
+            : base(logger)
+        {
+            _syncManager = syncManager;
+            _syncManager.SyncJobCancelled += _syncManager_SyncJobCancelled;
+            _syncManager.SyncJobCreated += _syncManager_SyncJobCreated;
+            _syncManager.SyncJobUpdated += _syncManager_SyncJobUpdated;
+        }
+
+        protected override void ParseMessageParams(string[] values)
+        {
+            base.ParseMessageParams(values);
+
+            if (values.Length > 0)
+            {
+                _userId = values[0];
+            }
+
+            if (values.Length > 1)
+            {
+                _targetId = values[1];
+            }
+        }
+
+        void _syncManager_SyncJobUpdated(object sender, Model.Events.GenericEventArgs<SyncJob> e)
+        {
+            SendData(false);
+        }
+
+        void _syncManager_SyncJobCreated(object sender, Model.Events.GenericEventArgs<SyncJobCreationResult> e)
+        {
+            SendData(true);
+        }
+
+        void _syncManager_SyncJobCancelled(object sender, Model.Events.GenericEventArgs<SyncJob> e)
+        {
+            SendData(true);
+        }
+
+        /// <summary>
+        /// Gets the data to send.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>Task{SystemInfo}.</returns>
+        protected override async Task<IEnumerable<SyncJob>> GetDataToSend(WebSocketListenerState state)
+        {
+            var jobs = await _syncManager.GetJobs(new SyncJobQuery
+            {
+                UserId = _userId,
+                TargetId = _targetId
+
+            }).ConfigureAwait(false);
+
+            return jobs.Items;
+        }
+
+        protected override bool SendOnTimer
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        protected override void Dispose(bool dispose)
+        {
+            _syncManager.SyncJobCancelled -= _syncManager_SyncJobCancelled;
+            _syncManager.SyncJobCreated -= _syncManager_SyncJobCreated;
+            _syncManager.SyncJobUpdated -= _syncManager_SyncJobUpdated;
+
+            base.Dispose(dispose);
+        }
+    }
+}

+ 93 - 5
MediaBrowser.Api/Sync/SyncService.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Sync;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Sync;
 using MediaBrowser.Model.Users;
@@ -38,6 +37,34 @@ namespace MediaBrowser.Api.Sync
     {
     }
 
+    [Route("/Sync/JobItems/{Id}/Enable", "POST", Summary = "Enables a cancelled or queued sync job item")]
+    public class EnableSyncJobItem : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sync/JobItems/{Id}/MarkForRemoval", "POST", Summary = "Marks a job item for removal")]
+    public class MarkJobItemForRemoval : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sync/JobItems/{Id}/UnmarkForRemoval", "POST", Summary = "Unmarks a job item for removal")]
+    public class UnmarkJobItemForRemoval : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sync/JobItems/{Id}", "DELETE", Summary = "Cancels a sync job item")]
+    public class CancelSyncJobItem : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+    }
+
     [Route("/Sync/Jobs", "GET", Summary = "Gets sync jobs.")]
     public class GetSyncJobs : SyncJobQuery, IReturn<QueryResult<SyncJob>>
     {
@@ -85,6 +112,16 @@ namespace MediaBrowser.Api.Sync
         public string Id { get; set; }
     }
 
+    [Route("/Sync/JobItems/{Id}/AdditionalFiles", "GET", Summary = "Gets a sync job item file")]
+    public class GetSyncJobItemAdditionalFile
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Name { get; set; }
+    }
+
     [Route("/Sync/OfflineActions", "POST", Summary = "Reports an action that occurred while offline.")]
     public class ReportOfflineActions : List<UserAction>, IReturnVoid
     {
@@ -169,11 +206,14 @@ namespace MediaBrowser.Api.Sync
         {
             var jobItem = _syncManager.GetJobItem(request.Id);
 
-            if (jobItem.Status != SyncJobItemStatus.Transferring)
+            if (jobItem.Status < SyncJobItemStatus.ReadyToTransfer)
             {
                 throw new ArgumentException("The job item is not yet ready for transfer.");
             }
 
+            var task = _syncManager.ReportSyncJobItemTransferBeginning(request.Id);
+            Task.WaitAll(task);
+
             return ToStaticFileResult(jobItem.OutputPath);
         }
 
@@ -198,10 +238,11 @@ namespace MediaBrowser.Api.Sync
                     }
                 };
 
-                var dtos = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                     .Select(_libraryManager.GetItemById)
-                    .Where(i => i != null)
-                    .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions))
+                    .Where(i => i != null);
+
+                var dtos = _dtoService.GetBaseItemDtos(items, dtoOptions)
                     .ToList();
 
                 result.Options = SyncHelper.GetSyncOptions(dtos);
@@ -243,5 +284,52 @@ namespace MediaBrowser.Api.Sync
 
             Task.WaitAll(task);
         }
+
+        public object Get(GetSyncJobItemAdditionalFile request)
+        {
+            var jobItem = _syncManager.GetJobItem(request.Id);
+
+            if (jobItem.Status < SyncJobItemStatus.ReadyToTransfer)
+            {
+                throw new ArgumentException("The job item is not yet ready for transfer.");
+            }
+
+            var file = jobItem.AdditionalFiles.FirstOrDefault(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
+
+            if (file == null)
+            {
+                throw new ArgumentException("Sync job additional file not found.");
+            }
+
+            return ToStaticFileResult(file.Path);
+        }
+
+        public void Post(EnableSyncJobItem request)
+        {
+            var task = _syncManager.ReEnableJobItem(request.Id);
+
+            Task.WaitAll(task);
+        }
+
+        public void Delete(CancelSyncJobItem request)
+        {
+            var task = _syncManager.CancelJobItem(request.Id);
+
+            Task.WaitAll(task);
+        }
+
+        public void Post(MarkJobItemForRemoval request)
+        {
+            var task = _syncManager.MarkJobItemForRemoval(request.Id);
+
+            Task.WaitAll(task);
+        }
+
+        public void Post(UnmarkJobItemForRemoval request)
+        {
+            var task = _syncManager.UnmarkJobItemForRemoval(request.Id);
+
+            Task.WaitAll(task);
+        }
     }
 }

+ 52 - 22
MediaBrowser.Api/TvShowsService.cs

@@ -156,6 +156,20 @@ namespace MediaBrowser.Api
 
         [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>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Limit { get; set; }
     }
 
     [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")]
@@ -238,7 +252,9 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         public object Get(GetSimilarShows request)
         {
-            var result = SimilarItemsHelper.GetSimilarItemsResult(_userManager,
+            var dtoOptions = GetDtoOptions(request);
+
+            var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager,
                 _itemRepo,
                 _libraryManager,
                 _userDataManager,
@@ -254,10 +270,10 @@ namespace MediaBrowser.Api
         {
             var user = _userManager.GetUserById(request.UserId);
 
-            var items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId)
-                .OfType<Episode>();
+            var items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, i => i is Episode);
 
-            var itemsList = _libraryManager.Sort(items, user, new[] { "PremiereDate", "AirTime", "SortName" }, SortOrder.Ascending)
+            var itemsList = _libraryManager
+                .Sort(items, user, new[] { "PremiereDate", "AirTime", "SortName" }, SortOrder.Ascending)
                 .Cast<Episode>()
                 .ToList();
 
@@ -270,9 +286,9 @@ namespace MediaBrowser.Api
 
             var pagedItems = ApplyPaging(previousEpisodes, request.StartIndex, request.Limit);
 
-            var options = request.GetDtoOptions();
+            var options = GetDtoOptions(request);
 
-            var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, options, user)).ToArray();
+            var returnItems = _dtoService.GetBaseItemDtos(pagedItems, options, user).ToArray();
 
             var result = new ItemsResult
             {
@@ -301,9 +317,9 @@ namespace MediaBrowser.Api
 
             var user = _userManager.GetUserById(request.UserId);
 
-            var options = request.GetDtoOptions();
+            var options = GetDtoOptions(request);
 
-            var returnItems = result.Items.Select(i => _dtoService.GetBaseItemDto(i, options, user)).ToArray();
+            var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user).ToArray();
 
             return ToOptimizedSerializedResultUsingCache(new ItemsResult
             {
@@ -365,9 +381,9 @@ namespace MediaBrowser.Api
                     .Cast<Season>();
             }
 
-            var fields = request.GetItemFields().ToList();
+            var dtoOptions = GetDtoOptions(request);
 
-            var returnItems = seasons.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+            var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user)
                 .ToArray();
 
             return new ItemsResult
@@ -411,7 +427,18 @@ namespace MediaBrowser.Api
 
             IEnumerable<Episode> episodes;
 
-            if (string.IsNullOrEmpty(request.SeasonId))
+            if (!string.IsNullOrWhiteSpace(request.SeasonId))
+            {
+                var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season;
+
+                if (season == null)
+                {
+                    throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
+                }
+
+                episodes = season.GetEpisodes(user);
+            } 
+            else if (request.Season.HasValue)
             {
                 var series = _libraryManager.GetItemById(request.Id) as Series;
 
@@ -424,14 +451,14 @@ namespace MediaBrowser.Api
             }
             else
             {
-                var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season;
+                var series = _libraryManager.GetItemById(request.Id) as Series;
 
-                if (season == null)
+                if (series == null)
                 {
-                    throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
+                    throw new ResourceNotFoundException("No series exists with Id " + request.Id);
                 }
 
-                episodes = season.GetEpisodes(user);
+                episodes = series.GetEpisodes(user);
             }
 
             // Filter after the fact in case the ui doesn't want them
@@ -448,24 +475,27 @@ namespace MediaBrowser.Api
                 episodes = episodes.Where(i => i.IsVirtualUnaired == val);
             }
 
+            IEnumerable<BaseItem> returnItems = episodes;
+
             // This must be the last filter
             if (!string.IsNullOrEmpty(request.AdjacentTo))
             {
-                episodes = UserViewBuilder.FilterForAdjacency(episodes, request.AdjacentTo)
-                    .Cast<Episode>();
+                returnItems = UserViewBuilder.FilterForAdjacency(returnItems, request.AdjacentTo);
             }
 
-            var fields = request.GetItemFields().ToList();
+            returnItems = _libraryManager.ReplaceVideosWithPrimaryVersions(returnItems);
 
-            episodes = _libraryManager.ReplaceVideosWithPrimaryVersions(episodes).Cast<Episode>();
+            var pagedItems = ApplyPaging(returnItems, request.StartIndex, request.Limit);
+            
+            var dtoOptions = GetDtoOptions(request);
 
-            var returnItems = episodes.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+            var dtos = _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user)
                 .ToArray();
 
             return new ItemsResult
             {
-                TotalRecordCount = returnItems.Length,
-                Items = returnItems
+                TotalRecordCount = dtos.Length,
+                Items = dtos
             };
         }
     }

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

@@ -83,7 +83,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetArtist(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {
@@ -130,8 +130,8 @@ namespace MediaBrowser.Api.UserLibrary
             if (request is GetAlbumArtists)
             {
                 return items
+                    .Where(i => !i.IsFolder)
                     .OfType<IHasAlbumArtist>()
-                    .Where(i => !(i is MusicAlbum))
                     .SelectMany(i => i.AlbumArtists)
                     .Distinct(StringComparer.OrdinalIgnoreCase)
                     .Select(name =>
@@ -150,8 +150,8 @@ namespace MediaBrowser.Api.UserLibrary
             }
 
             return items
+                .Where(i => !i.IsFolder)
                 .OfType<IHasArtist>()
-                .Where(i => !(i is MusicAlbum))
                 .SelectMany(i => i.AllArtists)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(name =>

+ 41 - 27
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -56,46 +56,52 @@ namespace MediaBrowser.Api.UserLibrary
         protected ItemsResult GetResult(GetItemsByName request)
         {
             User user = null;
-            BaseItem item;
+            BaseItem parentItem;
             List<BaseItem> libraryItems;
 
             if (request.UserId.HasValue)
             {
                 user = UserManager.GetUserById(request.UserId.Value);
-                item = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
-
+                parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
                 libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
 
             }
             else
             {
-                item = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-
-                libraryItems = LibraryManager.RootFolder.RecursiveChildren.ToList();
+                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
+                libraryItems = LibraryManager.RootFolder.GetRecursiveChildren().ToList();
             }
 
             IEnumerable<BaseItem> items;
 
-            if (item.IsFolder)
+            var excludeItemTypes = request.GetExcludeItemTypes();
+            var includeItemTypes = request.GetIncludeItemTypes();
+            var mediaTypes = request.GetMediaTypes();
+
+            Func<BaseItem, bool> filter = i => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
+
+            if (parentItem.IsFolder)
             {
-                var folder = (Folder)item;
+                var folder = (Folder)parentItem;
 
                 if (request.UserId.HasValue)
                 {
-                    items = request.Recursive ? folder.GetRecursiveChildren(user) : folder.GetChildren(user, true);
+                    items = request.Recursive ?
+                        folder.GetRecursiveChildren(user, filter) :
+                        folder.GetChildren(user, true).Where(filter);
                 }
                 else
                 {
-                    items = request.Recursive ? folder.GetRecursiveChildren() : folder.Children;
+                    items = request.Recursive ?
+                        folder.GetRecursiveChildren(filter) :
+                        folder.Children.Where(filter);
                 }
             }
             else
             {
-                items = new[] { item };
+                items = new[] { parentItem }.Where(filter);
             }
 
-            items = FilterItems(request, items);
-
             var extractedItems = GetAllItems(request, items);
 
             var filteredItems = FilterItems(request, extractedItems, user);
@@ -129,7 +135,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
 
-            var dtoOptions = request.GetDtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = tuples.Select(i => GetDto(i.Item1, user, dtoOptions, i.Item2));
 
@@ -290,33 +296,41 @@ namespace MediaBrowser.Api.UserLibrary
         /// Filters the items.
         /// </summary>
         /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
+        /// <param name="f">The f.</param>
+        /// <param name="excludeItemTypes">The exclude item types.</param>
+        /// <param name="includeItemTypes">The include item types.</param>
+        /// <param name="mediaTypes">The media types.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        protected virtual IEnumerable<BaseItem> FilterItems(GetItemsByName request, IEnumerable<BaseItem> items)
+        protected bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes)
         {
             // Exclude item types
-            if (!string.IsNullOrEmpty(request.ExcludeItemTypes))
+            if (excludeItemTypes.Length > 0)
             {
-                var vals = request.ExcludeItemTypes.Split(',');
-                items = items.Where(f => !vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
+                if (excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
             }
 
             // Include item types
-            if (!string.IsNullOrEmpty(request.IncludeItemTypes))
+            if (includeItemTypes.Length > 0)
             {
-                var vals = request.IncludeItemTypes.Split(',');
-                items = items.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
+                if (!includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
             }
 
             // Include MediaTypes
-            if (!string.IsNullOrEmpty(request.MediaTypes))
+            if (mediaTypes.Length > 0)
             {
-                var vals = request.MediaTypes.Split(',');
-
-                items = items.Where(f => vals.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase));
+                if (!mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
             }
 
-            return items;
+            return true;
         }
 
         /// <summary>

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

@@ -69,7 +69,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetGameGenre(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

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

@@ -74,7 +74,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetGenre(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

+ 14 - 39
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -169,8 +169,6 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string ExcludeLocationTypes { get; set; }
 
-        public bool IncludeIndexContainers { 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; }
 
@@ -321,14 +319,14 @@ namespace MediaBrowser.Api.UserLibrary
             var result = await GetItemsToSerialize(request, user, parentItem).ConfigureAwait(false);
 
             var isFiltered = result.Item2;
-            var dtoOptions = request.GetDtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (isFiltered)
             {
                 return new ItemsResult
                 {
                     TotalRecordCount = result.Item1.TotalRecordCount,
-                    Items = result.Item1.Items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray()
+                    Items = _dtoService.GetBaseItemDtos(result.Item1.Items, dtoOptions, user).ToArray()
                 };
             }
 
@@ -362,7 +360,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var pagedItems = ApplyPaging(request, itemsArray);
 
-            var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
+            var returnItems = _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user).ToArray();
 
             return new ItemsResult
             {
@@ -396,52 +394,29 @@ namespace MediaBrowser.Api.UserLibrary
 
             else if (request.Recursive)
             {
-                if (user == null)
-                {
-                    items = ((Folder)item).RecursiveChildren;
+                var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
 
-                    items = _libraryManager.ReplaceVideosWithPrimaryVersions(items);
-                }
-                else
-                {
-                    var result = await ((Folder)item).GetItems(GetItemsQuery(request, user));
-
-                    return new Tuple<QueryResult<BaseItem>, bool>(result, true);
-                }
+                return new Tuple<QueryResult<BaseItem>, bool>(result, true);
             }
             else
             {
                 if (user == null)
                 {
-                    items = ((Folder)item).Children;
+                    var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
 
-                    items = _libraryManager.ReplaceVideosWithPrimaryVersions(items);
+                    return new Tuple<QueryResult<BaseItem>, bool>(result, true);
                 }
-                else
-                {
-                    var userRoot = item as UserRootFolder;
 
-                    if (userRoot == null)
-                    {
-                        var result = await ((Folder)item).GetItems(GetItemsQuery(request, user));
+                var userRoot = item as UserRootFolder;
 
-                        return new Tuple<QueryResult<BaseItem>, bool>(result, true);
-                    }
+                if (userRoot == null)
+                {
+                    var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
 
-                    items = ((Folder)item).GetChildren(user, true);
+                    return new Tuple<QueryResult<BaseItem>, bool>(result, true);
                 }
-            }
-
-            if (request.IncludeIndexContainers)
-            {
-                var list = items.ToList();
-
-                var containers = list.Select(i => i.IndexContainer)
-                    .Where(i => i != null);
-
-                list.AddRange(containers);
 
-                items = list.Distinct();
+                items = ((Folder)item).GetChildren(user, true);
             }
 
             return new Tuple<QueryResult<BaseItem>, bool>(new QueryResult<BaseItem>
@@ -464,7 +439,7 @@ namespace MediaBrowser.Api.UserLibrary
                 SortBy = request.GetOrderBy(),
                 SortOrder = request.SortOrder ?? SortOrder.Ascending,
 
-                Filter = (i, u) => ApplyAdditionalFilters(request, i, u, true, _libraryManager),
+                Filter = i => ApplyAdditionalFilters(request, i, user, true, _libraryManager),
 
                 Limit = request.Limit,
                 StartIndex = request.StartIndex,

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

@@ -69,7 +69,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetMusicGenre(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

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

@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
@@ -86,7 +85,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetPerson(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

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

@@ -73,7 +73,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = GetStudio(request.Name, LibraryManager);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

+ 19 - 87
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -228,7 +228,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <value>The user id.</value>
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
+        public string UserId { get; set; }
 
         [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int Limit { get; set; }
@@ -259,7 +259,7 @@ namespace MediaBrowser.Api.UserLibrary
 
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
-        
+
         public GetLatestMedia()
         {
             Limit = 20;
@@ -304,74 +304,17 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var user = _userManager.GetUserById(request.UserId);
 
-            // Avoid implicitly captured closure
-            var libraryItems = string.IsNullOrEmpty(request.ParentId) && user != null ?
-                GetItemsConfiguredForLatest(user) :
-                GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId);
-
-            libraryItems = libraryItems.OrderByDescending(i => i.DateCreated)
-                .Where(i => i.LocationType != LocationType.Virtual);
-
-
-            //if (request.IsFolder.HasValue)
-            //{
-            //var val = request.IsFolder.Value;
-            libraryItems = libraryItems.Where(f => f.IsFolder == false);
-            //}
-
-            if (!string.IsNullOrEmpty(request.IncludeItemTypes))
-            {
-                var vals = request.IncludeItemTypes.Split(',');
-                libraryItems = libraryItems.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
-            }
-
-            var currentUser = user;
-
-            if (request.IsPlayed.HasValue)
-            {
-                var takeLimit = request.Limit * 20;
-
-                var val = request.IsPlayed.Value;
-                libraryItems = libraryItems.Where(f => f.IsPlayed(currentUser) == val)
-                    .Take(takeLimit);
-            }
-
-            // Avoid implicitly captured closure
-            var items = libraryItems
-                .ToList();
-
-            var list = new List<Tuple<BaseItem, List<BaseItem>>>();
-
-            foreach (var item in items)
+            var list = _userViewManager.GetLatestItems(new LatestItemsQuery
             {
-                // Only grab the index container for media
-                var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
-
-                if (container == null)
-                {
-                    list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
-                }
-                else
-                {
-                    var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
-
-                    if (current != null)
-                    {
-                        current.Item2.Add(item);
-                    }
-                    else
-                    {
-                        list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
-                    }
-                }
-
-                if (list.Count >= request.Limit)
-                {
-                    break;
-                }
-            }
+                GroupItems = request.GroupItems,
+                IncludeItemTypes = (request.IncludeItemTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
+                IsPlayed = request.IsPlayed,
+                Limit = request.Limit,
+                ParentId = request.ParentId,
+                UserId = request.UserId
+            });
 
-            var options = request.GetDtoOptions();
+            var options = GetDtoOptions(request);
 
             var dtos = list.Select(i =>
             {
@@ -394,15 +337,6 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(dtos.ToList());
         }
 
-        private IEnumerable<BaseItem> GetItemsConfiguredForLatest(User user)
-        {
-            return user.RootFolder.GetChildren(user, true)
-                .OfType<Folder>()
-                .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N")))
-                .SelectMany(i => i.GetRecursiveChildren(user))
-                .DistinctBy(i => i.Id);
-        }
-
         public async Task<object> Get(GetUserViews request)
         {
             var user = _userManager.GetUserById(request.UserId);
@@ -420,7 +354,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var folders = await _userViewManager.GetUserViews(query, CancellationToken.None).ConfigureAwait(false);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
                 .ToArray();
@@ -447,14 +381,13 @@ namespace MediaBrowser.Api.UserLibrary
             // Get them from the child tree
             if (series != null)
             {
-                var dtoOptions = new DtoOptions();
+                var dtoOptions = GetDtoOptions(request);
 
                 // Avoid implicitly captured closure
                 var currentUser = user;
 
                 var dtos = series
-                    .GetRecursiveChildren()
-                    .Where(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
+                    .GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
                     .OrderBy(i =>
                     {
                         if (i.PremiereDate.HasValue)
@@ -479,7 +412,7 @@ namespace MediaBrowser.Api.UserLibrary
             // Get them from the db
             if (movie != null)
             {
-                var dtoOptions = new DtoOptions();
+                var dtoOptions = GetDtoOptions(request);
 
                 var dtos = movie.SpecialFeatureIds
                     .Select(_libraryManager.GetItemById)
@@ -518,11 +451,10 @@ namespace MediaBrowser.Api.UserLibrary
                 trailerIds = hasTrailers.GetTrailerIds();
             }
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = trailerIds
                 .Select(_libraryManager.GetItemById)
-                .OrderBy(i => i.SortName)
                 .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
 
             return dtos.ToList();
@@ -539,7 +471,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _libraryManager.GetItemById(request.Id);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
 
@@ -557,7 +489,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var item = user.RootFolder;
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
 
@@ -577,7 +509,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
                 .ToArray();

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

@@ -73,7 +73,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var item = LibraryManager.GetYear(request.Year);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             if (request.UserId.HasValue)
             {

+ 110 - 6
MediaBrowser.Api/UserService.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Model.Connect;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Users;
 using ServiceStack;
-using ServiceStack.Text.Controller;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -56,6 +55,21 @@ namespace MediaBrowser.Api
         public string Id { get; set; }
     }
 
+    /// <summary>
+    /// Class GetUser
+    /// </summary>
+    [Route("/Users/{Id}/Offline", "GET", Summary = "Gets an offline user record by Id")]
+    [Authenticated]
+    public class GetOfflineUser : IReturn<UserDto>
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
     /// <summary>
     /// Class DeleteUser
     /// </summary>
@@ -148,6 +162,32 @@ namespace MediaBrowser.Api
         public bool ResetPassword { get; set; }
     }
 
+    /// <summary>
+    /// Class UpdateUserEasyPassword
+    /// </summary>
+    [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")]
+    [Authenticated]
+    public class UpdateUserEasyPassword : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the new password.
+        /// </summary>
+        /// <value>The new password.</value>
+        public string NewPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [reset password].
+        /// </summary>
+        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
+        public bool ResetPassword { get; set; }
+    }
+
     /// <summary>
     /// Class UpdateUser
     /// </summary>
@@ -294,7 +334,7 @@ namespace MediaBrowser.Api
                 .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
                 .ToList();
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return ToOptimizedResult(result);
         }
 
         /// <summary>
@@ -313,7 +353,23 @@ namespace MediaBrowser.Api
 
             var result = _userManager.GetUserDto(user, Request.RemoteIp);
 
-            return ToOptimizedSerializedResultUsingCache(result);
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetOfflineUser request)
+        {
+            var user = _userManager.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+
+            var result = _userManager.GetOfflineUserDto(user, auth.DeviceId);
+
+            return ToOptimizedResult(result);
         }
 
         /// <summary>
@@ -410,6 +466,8 @@ namespace MediaBrowser.Api
 
         public async Task PostAsync(UpdateUserPassword request)
         {
+            AssertCanUpdateUser(request.Id);
+
             var user = _userManager.GetUserById(request.Id);
 
             if (user == null)
@@ -434,6 +492,33 @@ namespace MediaBrowser.Api
             }
         }
 
+        public void Post(UpdateUserEasyPassword request)
+        {
+            var task = PostAsync(request);
+            Task.WaitAll(task);
+        }
+        
+        public async Task PostAsync(UpdateUserEasyPassword request)
+        {
+            AssertCanUpdateUser(request.Id);
+            
+            var user = _userManager.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            if (request.ResetPassword)
+            {
+                await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
+            }
+            else
+            {
+                await _userManager.ChangeEasyPassword(user, request.NewPassword).ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Posts the specified request.
         /// </summary>
@@ -449,14 +534,15 @@ namespace MediaBrowser.Api
         {
             // We need to parse this manually because we told service stack not to with IRequiresRequestStream
             // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var pathInfo = PathInfo.Parse(Request.PathInfo);
-            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+            var id = GetPathValue(1);
+
+            AssertCanUpdateUser(id);
 
             var dtoUser = request;
 
             var user = _userManager.GetUserById(id);
 
-            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ?
+            var task = string.Equals(user.Name, dtoUser.Name, StringComparison.Ordinal) ?
                 _userManager.UpdateUser(user) :
                 _userManager.RenameUser(user, dtoUser.Name);
 
@@ -500,11 +586,29 @@ namespace MediaBrowser.Api
 
         public void Post(UpdateUserConfiguration request)
         {
+            AssertCanUpdateUser(request.Id);
+
             var task = _userManager.UpdateConfiguration(request.Id, request);
 
             Task.WaitAll(task);
         }
 
+        private void AssertCanUpdateUser(string userId)
+        {
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+
+            // If they're going to update the record of another user, they must be an administrator
+            if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase))
+            {
+                var authenticatedUser = _userManager.GetUserById(auth.UserId);
+
+                if (!authenticatedUser.Policy.IsAdministrator)
+                {
+                    throw new SecurityException("Unauthorized access.");
+                }
+            }
+        }
+
         public void Post(UpdateUserPolicy request)
         {
             var task = UpdateUserPolicy(request);

+ 1 - 2
MediaBrowser.Api/VideosService.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System;
@@ -80,7 +79,7 @@ namespace MediaBrowser.Api
                                   : _libraryManager.RootFolder)
                            : _libraryManager.GetItemById(request.Id);
 
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = GetDtoOptions(request);
 
             var video = (Video)item;
 

+ 1 - 1
MediaBrowser.Common.Implementations/BaseApplicationHost.cs

@@ -475,7 +475,7 @@ namespace MediaBrowser.Common.Implementations
 			SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager);
 			RegisterSingleInstance(SecurityManager);
 
-			InstallationManager = new InstallationManager(Logger, this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager);
+			InstallationManager = new InstallationManager(Logger, this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager);
 			RegisterSingleInstance(InstallationManager);
 
 			ZipClient = new ZipClient();

+ 1 - 1
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -690,7 +690,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
         {
             try
             {
-                File.Delete(file);
+                _fileSystem.DeleteFile(file);
             }
             catch (IOException)
             {

+ 23 - 3
MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Model.Logging;
 using System;
@@ -270,8 +270,8 @@ namespace MediaBrowser.Common.Implementations.IO
             File.Copy(temp1, file2, true);
             File.Copy(temp2, file1, true);
 
-            File.Delete(temp1);
-            File.Delete(temp2);
+            DeleteFile(temp1);
+            DeleteFile(temp2);
         }
 
         /// <summary>
@@ -409,5 +409,25 @@ namespace MediaBrowser.Common.Implementations.IO
 
             //return Path.IsPathRooted(path);
         }
+
+        public void DeleteFile(string path, bool sendToRecycleBin)
+        {
+            File.Delete(path);
+        }
+
+        public void DeleteDirectory(string path, bool recursive, bool sendToRecycleBin)
+        {
+            Directory.Delete(path, recursive);
+        }
+
+        public void DeleteFile(string path)
+        {
+            DeleteFile(path, false);
+        }
+
+        public void DeleteDirectory(string path, bool recursive)
+        {
+            DeleteDirectory(path, recursive, false);
+        }
     }
 }

+ 6 - 6
MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj

@@ -48,21 +48,21 @@
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="NLog, Version=3.1.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
+    <Reference Include="NLog, Version=3.2.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\NLog.3.1.0.0\lib\net45\NLog.dll</HintPath>
+      <HintPath>..\packages\NLog.3.2.0.0\lib\net45\NLog.dll</HintPath>
     </Reference>
     <Reference Include="SharpCompress, Version=0.10.2.0, Culture=neutral, PublicKeyToken=beaf6f427e128133, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\ThirdParty\SharpCompress\SharpCompress.dll</HintPath>
     </Reference>
-    <Reference Include="SimpleInjector, Version=2.6.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
+    <Reference Include="SimpleInjector, Version=2.7.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\SimpleInjector.2.6.1\lib\net45\SimpleInjector.dll</HintPath>
+      <HintPath>..\packages\SimpleInjector.2.7.0\lib\net45\SimpleInjector.dll</HintPath>
     </Reference>
-    <Reference Include="SimpleInjector.Diagnostics, Version=2.6.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
+    <Reference Include="SimpleInjector.Diagnostics, Version=2.7.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\SimpleInjector.2.6.1\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
+      <HintPath>..\packages\SimpleInjector.2.7.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />

+ 73 - 3
MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs

@@ -18,11 +18,64 @@ namespace MediaBrowser.Common.Implementations.Networking
             Logger = logger;
         }
 
+        private volatile List<string> _localIpAddresses;
+        private readonly object _localIpAddressSyncLock = new object();
+
         /// <summary>
         /// Gets the machine's local ip address
         /// </summary>
         /// <returns>IPAddress.</returns>
         public IEnumerable<string> GetLocalIpAddresses()
+        {
+            if (_localIpAddresses == null)
+            {
+                lock (_localIpAddressSyncLock)
+                {
+                    if (_localIpAddresses == null)
+                    {
+                        var addresses = GetLocalIpAddressesInternal().ToList();
+
+                        _localIpAddresses = addresses;
+                        BindEvents();
+
+                        return addresses;
+                    }
+                }
+            }
+
+            return _localIpAddresses;
+        }
+
+        private void BindEvents()
+        {
+            NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
+            NetworkChange.NetworkAvailabilityChanged -= NetworkChange_NetworkAvailabilityChanged;
+
+            NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
+            NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
+        }
+
+        void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
+        {
+            Logger.Debug("NetworkAvailabilityChanged fired. Resetting cached network info.");
+
+            lock (_localIpAddressSyncLock)
+            {
+                _localIpAddresses = null;
+            }
+        }
+
+        void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
+        {
+            Logger.Debug("NetworkAddressChanged fired. Resetting cached network info.");
+
+            lock (_localIpAddressSyncLock)
+            {
+                _localIpAddresses = null;
+            }
+        }
+
+        private IEnumerable<string> GetLocalIpAddressesInternal()
         {
             var list = GetIPsDefault()
                 .Where(i => !IPAddress.IsLoopback(i))
@@ -53,6 +106,11 @@ namespace MediaBrowser.Common.Implementations.Networking
             // Private address space:
             // http://en.wikipedia.org/wiki/Private_network
 
+            if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase))
+            {
+                return Is172AddressPrivate(endpoint);
+            }
+
             return
 
                 // If url was requested with computer name, we may see this
@@ -61,11 +119,23 @@ namespace MediaBrowser.Common.Implementations.Networking
                 endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
                 endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
                 endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
-                endpoint.StartsWith("192.", StringComparison.OrdinalIgnoreCase) ||
-                endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase) ||
+                endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase) ||
                 endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
         }
 
+        private bool Is172AddressPrivate(string endpoint)
+        {
+            for (var i = 16; i <= 31; i++)
+            {
+                if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         public bool IsInLocalNetwork(string endpoint)
         {
             return IsInLocalNetworkInternal(endpoint, true);
@@ -122,7 +192,7 @@ namespace MediaBrowser.Common.Implementations.Networking
 
             return false;
         }
-        
+
         public IEnumerable<IPAddress> GetIpAddresses(string hostName)
         {
             return Dns.GetHostAddresses(hostName);

+ 110 - 40
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -108,13 +108,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         private TaskResult _lastExecutionResult;
         /// <summary>
-        /// The _last execution resultinitialized
-        /// </summary>
-        private bool _lastExecutionResultinitialized;
-        /// <summary>
         /// The _last execution result sync lock
         /// </summary>
-        private object _lastExecutionResultSyncLock = new object();
+        private readonly object _lastExecutionResultSyncLock = new object();
         /// <summary>
         /// Gets the last execution result.
         /// </summary>
@@ -123,38 +119,39 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             get
             {
-                LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () =>
+                if (_lastExecutionResult == null)
                 {
-                    var path = GetHistoryFilePath();
-
-                    try
-                    {
-                        return JsonSerializer.DeserializeFromFile<TaskResult>(path);
-                    }
-                    catch (DirectoryNotFoundException)
-                    {
-                        // File doesn't exist. No biggie
-                        return null;
-                    }
-                    catch (FileNotFoundException)
+                    lock (_lastExecutionResultSyncLock)
                     {
-                        // File doesn't exist. No biggie
-                        return null;
+                        if (_lastExecutionResult == null)
+                        {
+                            var path = GetHistoryFilePath();
+
+                            try
+                            {
+                                return JsonSerializer.DeserializeFromFile<TaskResult>(path);
+                            }
+                            catch (DirectoryNotFoundException)
+                            {
+                                // File doesn't exist. No biggie
+                            }
+                            catch (FileNotFoundException)
+                            {
+                                // File doesn't exist. No biggie
+                            }
+                            catch (Exception ex)
+                            {
+                                Logger.ErrorException("Error deserializing {0}", ex, path);
+                            }
+                        }
                     }
-                    catch (Exception ex)
-                    {
-                        Logger.ErrorException("Error deserializing {0}", ex, path);
-                        return null;
-                    }
-                });
+                }
 
                 return _lastExecutionResult;
             }
             private set
             {
                 _lastExecutionResult = value;
-
-                _lastExecutionResultinitialized = value != null;
             }
         }
 
@@ -227,13 +224,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         private IEnumerable<ITaskTrigger> _triggers;
         /// <summary>
-        /// The _triggers initialized
-        /// </summary>
-        private bool _triggersInitialized;
-        /// <summary>
         /// The _triggers sync lock
         /// </summary>
-        private object _triggersSyncLock = new object();
+        private readonly object _triggersSyncLock = new object();
         /// <summary>
         /// Gets the triggers that define when the task will run
         /// </summary>
@@ -243,7 +236,16 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             get
             {
-                LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, LoadTriggers);
+                if (_triggers == null)
+                {
+                    lock (_triggersSyncLock)
+                    {
+                        if (_triggers == null)
+                        {
+                            _triggers = LoadTriggers();
+                        }
+                    }
+                }
 
                 return _triggers;
             }
@@ -262,8 +264,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
 
                 _triggers = value.ToList();
 
-                _triggersInitialized = true;
-
                 ReloadTriggerEvents(false);
 
                 SaveTriggers(_triggers);
@@ -335,12 +335,30 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             trigger.Start(false);
         }
 
+        private Task _currentTask;
+
         /// <summary>
         /// Executes the task
         /// </summary>
         /// <returns>Task.</returns>
         /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
         public async Task Execute()
+        {
+            var task = ExecuteInternal();
+
+            _currentTask = task;
+
+            try
+            {
+                await task.ConfigureAwait(false);
+            }
+            finally
+            {
+                _currentTask = null;
+            }
+        }
+
+        private async Task ExecuteInternal()
         {
             // Cancel the current execution, if any
             if (CurrentCancellationTokenSource != null)
@@ -544,6 +562,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
                 Id = Id
             };
 
+            var hasKey = ScheduledTask as IHasKey;
+            if (hasKey != null)
+            {
+                result.Key = hasKey.Key;
+            }
+
             if (ex != null)
             {
                 result.ErrorMessage = ex.Message;
@@ -579,14 +603,60 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             {
                 DisposeTriggers();
 
-                if (State == TaskState.Running)
+                var wassRunning = State == TaskState.Running;
+                var startTime = CurrentExecutionStartTime;
+
+                var token = CurrentCancellationTokenSource;
+                if (token != null)
                 {
-                    OnTaskCompleted(CurrentExecutionStartTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
+                    try
+                    {
+                        Logger.Debug(Name + ": Cancelling");
+                        token.Cancel();
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.ErrorException("Error calling CancellationToken.Cancel();", ex);
+                    }
+                }
+                var task = _currentTask;
+                if (task != null)
+                {
+                    try
+                    {
+                        Logger.Debug(Name + ": Waiting on Task");
+                        var exited = Task.WaitAll(new[] { task }, 2000);
+
+                        if (exited)
+                        {
+                            Logger.Debug(Name + ": Task exited");
+                        }
+                        else
+                        {
+                            Logger.Debug(Name + ": Timed out waiting for task to stop");
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.ErrorException("Error calling Task.WaitAll();", ex);
+                    }
                 }
 
-                if (CurrentCancellationTokenSource != null)
+                if (token != null)
+                {
+                    try
+                    {
+                        Logger.Debug(Name + ": Disposing CancellationToken");
+                        token.Dispose();
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.ErrorException("Error calling CancellationToken.Dispose();", ex);
+                    }
+                }
+                if (wassRunning)
                 {
-                    CurrentCancellationTokenSource.Dispose();
+                    OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
                 }
             }
         }

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

@@ -125,7 +125,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         {
             try
             {
-                File.Delete(path);
+                _fileSystem.DeleteFile(path);
             }
             catch (IOException ex)
             {

+ 1 - 1
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs

@@ -76,7 +76,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                File.Delete(file.FullName);
+                _fileSystem.DeleteFile(file.FullName);
 
                 index++;
             }

+ 6 - 3
MediaBrowser.Common.Implementations/Updates/InstallationManager.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Implementations.Security;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Progress;
@@ -106,6 +107,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         private readonly IJsonSerializer _jsonSerializer;
         private readonly ISecurityManager _securityManager;
         private readonly IConfigurationManager _config;
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Gets the application host.
@@ -113,7 +115,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         /// <value>The application host.</value>
         private readonly IApplicationHost _applicationHost;
 
-        public InstallationManager(ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, IJsonSerializer jsonSerializer, ISecurityManager securityManager, IConfigurationManager config)
+        public InstallationManager(ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, IJsonSerializer jsonSerializer, ISecurityManager securityManager, IConfigurationManager config, IFileSystem fileSystem)
         {
             if (logger == null)
             {
@@ -129,6 +131,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             _jsonSerializer = jsonSerializer;
             _securityManager = securityManager;
             _config = config;
+            _fileSystem = fileSystem;
             _logger = logger;
         }
 
@@ -570,7 +573,7 @@ namespace MediaBrowser.Common.Implementations.Updates
 
             try
             {
-                File.Delete(tempFile);
+                _fileSystem.DeleteFile(tempFile);
             }
             catch (IOException e)
             {
@@ -591,7 +594,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             // Remove it the quick way for now
             _applicationHost.RemovePlugin(plugin);
 
-            File.Delete(plugin.AssemblyFilePath);
+            _fileSystem.DeleteFile(plugin.AssemblyFilePath);
 
             OnPluginUninstalled(plugin);
 

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

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="NLog" version="3.1.0.0" targetFramework="net45" />
-  <package id="SimpleInjector" version="2.6.1" targetFramework="net45" />
+  <package id="NLog" version="3.2.0.0" targetFramework="net45" />
+  <package id="SimpleInjector" version="2.7.0" targetFramework="net45" />
 </packages>

+ 0 - 30
MediaBrowser.Common/Extensions/BaseExtensions.cs

@@ -26,36 +26,6 @@ namespace MediaBrowser.Common.Extensions
             return Regex.Replace(htmlString, pattern, string.Empty).Trim();
         }
 
-        /// <summary>
-        /// Replaces the specified STR.
-        /// </summary>
-        /// <param name="str">The STR.</param>
-        /// <param name="oldValue">The old value.</param>
-        /// <param name="newValue">The new value.</param>
-        /// <param name="comparison">The comparison.</param>
-        /// <returns>System.String.</returns>
-        public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
-        {
-            var sb = new StringBuilder();
-
-            var previousIndex = 0;
-            var index = str.IndexOf(oldValue, comparison);
-
-            while (index != -1)
-            {
-                sb.Append(str.Substring(previousIndex, index - previousIndex));
-                sb.Append(newValue);
-                index += oldValue.Length;
-
-                previousIndex = index;
-                index = str.IndexOf(oldValue, index, comparison);
-            }
-
-            sb.Append(str.Substring(previousIndex));
-
-            return sb.ToString();
-        }
-
         public static string RemoveDiacritics(this string text)
         {
             return String.Concat(

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

@@ -133,5 +133,33 @@ namespace MediaBrowser.Common.IO
         /// <param name="path">The path.</param>
         /// <returns><c>true</c> if [is path file] [the specified path]; otherwise, <c>false</c>.</returns>
         bool IsPathFile(string path);
+
+        /// <summary>
+        /// Deletes the file.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="sendToRecycleBin">if set to <c>true</c> [send to recycle bin].</param>
+        void DeleteFile(string path, bool sendToRecycleBin);
+
+        /// <summary>
+        /// Deletes the directory.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        /// <param name="sendToRecycleBin">if set to <c>true</c> [send to recycle bin].</param>
+        void DeleteDirectory(string path, bool recursive, bool sendToRecycleBin);
+
+        /// <summary>
+        /// Deletes the file.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        void DeleteFile(string path);
+
+        /// <summary>
+        /// Deletes the directory.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        void DeleteDirectory(string path, bool recursive);
     }
 }

+ 7 - 0
MediaBrowser.Common/Net/INetworkManager.cs

@@ -51,5 +51,12 @@ namespace MediaBrowser.Common.Net
         /// <param name="endpoint">The endpoint.</param>
         /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
         bool IsInLocalNetwork(string endpoint);
+
+        /// <summary>
+        /// Generates a self signed certificate at the locatation specified by <paramref name="certificatePath"/>.
+        /// </summary>
+        /// <param name="certificatePath">The path to generate the certificate.</param>
+        /// <param name="hostname">The common name for the certificate.</param>
+        void GenerateSelfSignedSslCertificate(string certificatePath, string hostname);
     }
 }

+ 29 - 3
MediaBrowser.Controller/Channels/Channel.cs

@@ -5,6 +5,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -14,9 +15,19 @@ namespace MediaBrowser.Controller.Channels
 
         public override bool IsVisible(User user)
         {
-            if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+            if (user.Policy.BlockedChannels != null)
             {
-                return false;
+                if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
             }
             
             return base.IsVisible(user);
@@ -50,7 +61,22 @@ namespace MediaBrowser.Controller.Channels
 
         protected override string GetInternalMetadataPath(string basePath)
         {
-            return System.IO.Path.Combine(basePath, "channels", Id.ToString("N"), "metadata");
+            return GetInternalMetadataPath(basePath, Id);
+        }
+
+        public static string GetInternalMetadataPath(string basePath, Guid id)
+        {
+            return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata");
+        }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
+        protected override bool IsAllowTagFilterEnforced()
+        {
+            return false;
         }
     }
 }

+ 7 - 2
MediaBrowser.Controller/Channels/ChannelAudioItem.cs

@@ -3,10 +3,10 @@ using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Channels
             return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent);
         }
 
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return ExternalId;
         }
@@ -89,5 +89,10 @@ namespace MediaBrowser.Controller.Channels
 
             return list;
         }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
     }
 }

+ 9 - 5
MediaBrowser.Controller/Channels/ChannelFolderItem.cs

@@ -1,11 +1,10 @@
-using System;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Channels;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Users;
+using System;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -40,7 +39,7 @@ namespace MediaBrowser.Controller.Channels
             return false;
         }
 
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return ExternalId;
         }
@@ -76,5 +75,10 @@ namespace MediaBrowser.Controller.Channels
         {
             return System.IO.Path.Combine(basePath, "channels", ChannelId, Id.ToString("N"));
         }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
     }
 }

+ 8 - 3
MediaBrowser.Controller/Channels/ChannelVideoItem.cs

@@ -4,11 +4,11 @@ using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -28,8 +28,8 @@ namespace MediaBrowser.Controller.Channels
         public string OriginalImageUrl { get; set; }
 
         public List<ChannelMediaInfo> ChannelMediaSources { get; set; }
-        
-        public override string GetUserDataKey()
+
+        protected override string CreateUserDataKey()
         {
             if (ContentType == ChannelMediaContentType.MovieExtra)
             {
@@ -119,5 +119,10 @@ namespace MediaBrowser.Controller.Channels
         {
             return System.IO.Path.Combine(basePath, "channels", ChannelId, Id.ToString("N"));
         }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
     }
 }

+ 7 - 0
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -60,5 +60,12 @@ namespace MediaBrowser.Controller.Collections
         /// <param name="userId">The user identifier.</param>
         /// <returns>Folder.</returns>
         Folder GetCollectionsFolder(string userId);
+
+        /// <summary>
+        /// Gets the collections.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable&lt;BoxSet&gt;.</returns>
+        IEnumerable<BoxSet> GetCollections(User user);
     }
 }

+ 10 - 0
MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs

@@ -0,0 +1,10 @@
+using MediaBrowser.Model.Devices;
+
+namespace MediaBrowser.Controller.Devices
+{
+    public class CameraImageUploadInfo
+    {
+        public LocalFileInfo FileInfo { get; set; }
+        public DeviceInfo Device { get; set; }
+    }
+}

+ 4 - 1
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
 
@@ -15,6 +14,10 @@ namespace MediaBrowser.Controller.Devices
         /// Occurs when [device options updated].
         /// </summary>
         event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated;
+        /// <summary>
+        /// Occurs when [camera image uploaded].
+        /// </summary>
+        event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
 
         /// <summary>
         /// Registers the device.

+ 2 - 1
MediaBrowser.Controller/Dlna/IDlnaManager.cs

@@ -62,8 +62,9 @@ namespace MediaBrowser.Controller.Dlna
         /// </summary>
         /// <param name="headers">The headers.</param>
         /// <param name="serverUuId">The server uu identifier.</param>
+        /// <param name="serverAddress">The server address.</param>
         /// <returns>System.String.</returns>
-        string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId);
+        string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress);
 
         /// <summary>
         /// Gets the icon.

+ 7 - 0
MediaBrowser.Controller/Dlna/IMediaReceiverRegistrar.cs

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Dlna
+{
+    public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
+    {
+    }
+}

+ 1 - 0
MediaBrowser.Controller/Dto/DtoOptions.cs

@@ -17,6 +17,7 @@ namespace MediaBrowser.Controller.Dto
         public List<ImageType> ImageTypes { get; set; }
         public int ImageTypeLimit { get; set; }
         public bool EnableImages { get; set; }
+        public string DeviceId { get; set; }
 
         public DtoOptions()
         {

+ 11 - 0
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -44,6 +44,17 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="owner">The owner.</param>
         /// <returns>BaseItemDto.</returns>
         BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+
+        /// <summary>
+        /// Gets the base item dtos.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="owner">The owner.</param>
+        /// <returns>IEnumerable&lt;BaseItemDto&gt;.</returns>
+        IEnumerable<BaseItemDto> GetBaseItemDtos(IEnumerable<BaseItem> items, DtoOptions options, User user = null,
+            BaseItem owner = null);
         
         /// <summary>
         /// Gets the chapter information dto.

+ 0 - 19
MediaBrowser.Controller/Entities/AdultVideo.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Entities
-{
-    [Obsolete]
-    public class AdultVideo : Video, IHasProductionLocations, IHasTaglines
-    {
-        public List<string> ProductionLocations { get; set; }
-
-        public List<string> Taglines { get; set; }
-
-        public AdultVideo()
-        {
-            Taglines = new List<string>();
-            ProductionLocations = new List<string>();
-        }
-    }
-}

+ 6 - 1
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -32,6 +32,11 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         /// <summary>
         /// The _virtual children
         /// </summary>
@@ -66,7 +71,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var path = ContainingFolderPath;
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager, directoryService)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths , directoryService)
             {
                 FileInfo = new DirectoryInfo(path),
                 Path = path,

+ 20 - 4
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -4,11 +4,11 @@ using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.Serialization;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -80,6 +80,15 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
+        [IgnoreDataMember]
+        protected override bool SupportsOwnedItems
+        {
+            get
+            {
+                return false;
+            }
+        }
+
         [IgnoreDataMember]
         public override Folder LatestItemsIndexContainer
         {
@@ -104,6 +113,13 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
+        public override bool CanDownload()
+        {
+            var locationType = LocationType;
+            return locationType != LocationType.Remote &&
+                   locationType != LocationType.Virtual;
+        }
+
         /// <summary>
         /// Gets or sets the artist.
         /// </summary>
@@ -169,7 +185,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             var parent = FindParent<MusicAlbum>();
 
@@ -186,7 +202,7 @@ namespace MediaBrowser.Controller.Entities.Audio
                 }
             }
 
-            return base.GetUserDataKey();
+            return base.CreateUserDataKey();
         }
 
         protected override bool GetBlockUnratedValue(UserPolicy config)
@@ -223,7 +239,7 @@ namespace MediaBrowser.Controller.Entities.Audio
             {
                 Id = i.Id.ToString("N"),
                 Protocol = locationType == LocationType.Remote ? MediaProtocol.Http : MediaProtocol.File,
-                MediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(),
+                MediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(),
                 Name = i.Name,
                 Path = enablePathSubstituion ? GetMappedPath(i.Path, locationType) : i.Path,
                 RunTimeTicks = i.RunTimeTicks,

+ 8 - 13
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -1,11 +1,11 @@
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.Serialization;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -52,14 +52,14 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
-        public List<string> AlbumArtists { get; set; }
-
         [IgnoreDataMember]
         public string AlbumArtist
         {
             get { return AlbumArtists.FirstOrDefault(); }
         }
 
+        public List<string> AlbumArtists { get; set; }
+
         /// <summary>
         /// Gets the tracks.
         /// </summary>
@@ -68,7 +68,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         {
             get
             {
-                return RecursiveChildren.OfType<Audio>();
+                return GetRecursiveChildren(i => i is Audio).Cast<Audio>();
             }
         }
 
@@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             var id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
 
@@ -152,7 +152,7 @@ namespace MediaBrowser.Controller.Entities.Audio
                 return "MusicAlbum-Musicbrainz-" + id;
             }
 
-            return base.GetUserDataKey();
+            return base.CreateUserDataKey();
         }
 
         protected override bool GetBlockUnratedValue(UserPolicy config)
@@ -173,17 +173,12 @@ namespace MediaBrowser.Controller.Entities.Audio
                 id.ArtistProviderIds = artist.ProviderIds;
             }
 
-            id.SongInfos = RecursiveChildren.OfType<Audio>()
+            id.SongInfos = GetRecursiveChildren(i => i is Audio)
+                .Cast<Audio>()
                 .Select(i => i.GetLookupInfo())
                 .ToList();
 
             return id;
         }
     }
-
-    [Obsolete]
-    public class MusicAlbumDisc : Folder
-    {
-
-    }
 }

+ 38 - 60
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -1,14 +1,13 @@
-using System.Runtime.Serialization;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -19,7 +18,8 @@ namespace MediaBrowser.Controller.Entities.Audio
     {
         public bool IsAccessedByName { get; set; }
         public List<string> ProductionLocations { get; set; }
-        
+
+        [IgnoreDataMember]
         public override bool IsFolder
         {
             get
@@ -34,6 +34,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             get { return true; }
         }
 
+        public override bool CanDelete()
+        {
+            return !IsAccessedByName;
+        }
+
         protected override IEnumerable<BaseItem> ActualChildren
         {
             get
@@ -68,7 +73,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return GetUserDataKey(this);
         }
@@ -78,6 +83,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// If the item is a folder, it returns the folder itself
         /// </summary>
         /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
         public override string ContainingFolderPath
         {
             get
@@ -90,6 +96,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// Gets a value indicating whether this instance is owned item.
         /// </summary>
         /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
         public override bool IsOwnedItem
         {
             get
@@ -122,89 +129,53 @@ namespace MediaBrowser.Controller.Entities.Audio
 
         public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
         {
-            var items = RecursiveChildren.ToList();
+            var items = GetRecursiveChildren().ToList();
 
             var songs = items.OfType<Audio>().ToList();
 
             var others = items.Except(songs).ToList();
 
             var totalItems = songs.Count + others.Count;
-            var percentages = new Dictionary<Guid, double>(totalItems);
-
-            var tasks = new List<Task>();
+            var numComplete = 0;
 
             // Refresh songs
             foreach (var item in songs)
             {
-                if (tasks.Count >= 2)
-                {
-                    await Task.WhenAll(tasks).ConfigureAwait(false);
-                    tasks.Clear();
-                }
-
                 cancellationToken.ThrowIfCancellationRequested();
-                var innerProgress = new ActionableProgress<double>();
 
-                // Avoid implicitly captured closure
-                var currentChild = item;
-                innerProgress.RegisterAction(p =>
-                {
-                    lock (percentages)
-                    {
-                        percentages[currentChild.Id] = p / 100;
-
-                        var percent = percentages.Values.Sum();
-                        percent /= totalItems;
-                        percent *= 100;
-                        progress.Report(percent);
-                    }
-                });
-
-                var taskChild = item;
-                tasks.Add(Task.Run(async () => await RefreshItem(taskChild, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken));
-            }
+                await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
-            tasks.Clear();
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
+            }
 
             // Refresh current item
             await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-            
+
             // Refresh all non-songs
             foreach (var item in others)
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                // Avoid implicitly captured closure
-                var currentChild = item;
-
                 await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-                lock (percentages)
-                {
-                    percentages[currentChild.Id] = 1;
 
-                    var percent = percentages.Values.Sum();
-                    percent /= totalItems;
-                    percent *= 100;
-                    progress.Report(percent);
-                }
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
             }
 
             progress.Report(100);
         }
 
-        private async Task RefreshItem(BaseItem item, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-
-            progress.Report(100);
-        }
-
         public ArtistInfo GetLookupInfo()
         {
             var info = GetItemLookupInfo<ArtistInfo>();
 
-            info.SongInfos = RecursiveChildren.OfType<Audio>()
+            info.SongInfos = GetRecursiveChildren(i => i is Audio)
+                .Cast<Audio>()
                 .Select(i => i.GetLookupInfo())
                 .ToList();
 
@@ -213,9 +184,16 @@ namespace MediaBrowser.Controller.Entities.Audio
 
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         {
-            return inputItems.OfType<IHasArtist>()
-                .Where(i => i.HasArtist(Name))
-                .Cast<BaseItem>();
+            return inputItems.Where(GetItemFilter());
+        }
+
+        public Func<BaseItem, bool> GetItemFilter()
+        {
+            return i =>
+            {
+                var hasArtist = i as IHasArtist;
+                return hasArtist != null && hasArtist.HasArtist(Name);
+            };
         }
     }
 }

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

@@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return "MusicGenre-" + Name;
         }
@@ -30,6 +30,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// If the item is a folder, it returns the folder itself
         /// </summary>
         /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
         public override string ContainingFolderPath
         {
             get
@@ -38,10 +39,16 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         /// <summary>
         /// Gets a value indicating whether this instance is owned item.
         /// </summary>
         /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
         public override bool IsOwnedItem
         {
             get
@@ -52,7 +59,12 @@ namespace MediaBrowser.Controller.Entities.Audio
 
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         {
-            return inputItems.Where(i => (i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+            return inputItems.Where(GetItemFilter());
+        }
+
+        public Func<BaseItem, bool> GetItemFilter()
+        {
+            return i => (i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase);
         }
     }
 }

+ 99 - 16
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -239,6 +239,38 @@ namespace MediaBrowser.Controller.Entities
             get { return this.GetImagePath(ImageType.Primary); }
         }
 
+        public virtual bool CanDelete()
+        {
+            var locationType = LocationType;
+            return locationType != LocationType.Remote &&
+                   locationType != LocationType.Virtual;
+        }
+
+        public virtual bool IsAuthorizedToDelete(User user)
+        {
+            return user.Policy.EnableContentDeletion;
+        }
+
+        public bool CanDelete(User user)
+        {
+            return CanDelete() && IsAuthorizedToDelete(user);
+        }
+
+        public virtual bool CanDownload()
+        {
+            return false;
+        }
+
+        public virtual bool IsAuthorizedToDownload(User user)
+        {
+            return user.Policy.EnableContentDownloading;
+        }
+
+        public bool CanDownload(User user)
+        {
+            return CanDownload() && IsAuthorizedToDownload(user);
+        }
+
         /// <summary>
         /// Gets or sets the date created.
         /// </summary>
@@ -268,6 +300,7 @@ namespace MediaBrowser.Controller.Entities
         public static IChannelManager ChannelManager { get; set; }
         public static ICollectionManager CollectionManager { get; set; }
         public static IImageProcessor ImageProcessor { get; set; }
+        public static IMediaSourceManager MediaSourceManager { get; set; }
 
         /// <summary>
         /// Returns a <see cref="System.String" /> that represents this instance.
@@ -359,7 +392,7 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (!string.IsNullOrEmpty(ForcedSortName))
+                if (!string.IsNullOrWhiteSpace(ForcedSortName))
                 {
                     return ForcedSortName;
                 }
@@ -379,21 +412,19 @@ namespace MediaBrowser.Controller.Entities
 
         public string GetInternalMetadataPath()
         {
-            return GetInternalMetadataPath(ConfigurationManager.ApplicationPaths.InternalMetadataPath);
+            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+
+            return GetInternalMetadataPath(basePath);
         }
 
         protected virtual string GetInternalMetadataPath(string basePath)
         {
             var idString = Id.ToString("N");
 
-            return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
-        }
-
-        public static string GetInternalMetadataPathForId(Guid id)
-        {
-            var idString = id.ToString("N");
-
-            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+            if (ConfigurationManager.Configuration.EnableLibraryMetadataSubFolder)
+            {
+                basePath = System.IO.Path.Combine(basePath, "library");
+            }
 
             return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
         }
@@ -692,7 +723,7 @@ namespace MediaBrowser.Controller.Entities
 
             var requiresSave = false;
 
-            if (IsFolder || Parent != null)
+            if (SupportsOwnedItems)
             {
                 try
                 {
@@ -724,6 +755,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        [IgnoreDataMember]
+        protected virtual bool SupportsOwnedItems
+        {
+            get { return IsFolder || Parent != null; }
+        }
+
         /// <summary>
         /// Refreshes owned items such as trailers, theme videos, special features, etc.
         /// Returns true or false indicating if changes were found.
@@ -889,11 +926,24 @@ namespace MediaBrowser.Controller.Entities
             get { return null; }
         }
 
+        private string _userDataKey;
         /// <summary>
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public virtual string GetUserDataKey()
+        public string GetUserDataKey()
+        {
+            if (string.IsNullOrWhiteSpace(_userDataKey))
+            {
+                var key = CreateUserDataKey();
+                _userDataKey = key;
+                return key;
+            }
+
+            return _userDataKey;
+        }
+
+        protected virtual string CreateUserDataKey()
         {
             return Id.ToString();
         }
@@ -905,6 +955,12 @@ namespace MediaBrowser.Controller.Entities
             return current.IsInMixedFolder == newItem.IsInMixedFolder;
         }
 
+        public void AfterMetadataRefresh()
+        {
+            _sortName = null;
+            _userDataKey = null;
+        }
+
         /// <summary>
         /// Gets the preferred metadata language.
         /// </summary>
@@ -1024,7 +1080,8 @@ namespace MediaBrowser.Controller.Entities
 
             if (hasTags != null)
             {
-                if (user.Policy.BlockedTags.Any(i => hasTags.Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
+                var policy = user.Policy;
+                if (policy.BlockedTags.Any(i => hasTags.Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
                 {
                     return false;
                 }
@@ -1033,6 +1090,11 @@ namespace MediaBrowser.Controller.Entities
             return true;
         }
 
+        protected virtual bool IsAllowTagFilterEnforced()
+        {
+            return true;
+        }
+
         /// <summary>
         /// Gets the block unrated value.
         /// </summary>
@@ -1060,6 +1122,23 @@ namespace MediaBrowser.Controller.Entities
             return IsParentalAllowed(user);
         }
 
+        public virtual bool IsVisibleStandalone(User user)
+        {
+            if (!IsVisible(user))
+            {
+                return false;
+            }
+
+            if (Parents.Any(i => !i.IsVisible(user)))
+            {
+                return false;
+            }
+
+            // TODO: Need some work here, e.g. is in user library, for channels, can user access channel, etc.
+
+            return true;
+        }
+
         /// <summary>
         /// Gets a value indicating whether this instance is folder.
         /// </summary>
@@ -1146,7 +1225,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType))
             {
-                return LibraryManager.RootFolder.RecursiveChildren.FirstOrDefault(i =>
+                return LibraryManager.RootFolder.GetRecursiveChildren(i =>
                 {
                     if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase))
                     {
@@ -1164,7 +1243,8 @@ namespace MediaBrowser.Controller.Entities
                     }
 
                     return false;
-                });
+
+                }).FirstOrDefault();
             }
 
             return null;
@@ -1458,7 +1538,7 @@ namespace MediaBrowser.Controller.Entities
                     currentFile.Attributes &= ~FileAttributes.Hidden;
                 }
 
-                currentFile.Delete();
+                FileSystem.DeleteFile(currentFile.FullName);
             }
 
             return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
@@ -1703,6 +1783,9 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public virtual bool BeforeMetadataRefresh()
         {
+            _userDataKey = null;
+            _sortName = null;
+
             var hasChanges = false;
 
             if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))

+ 5 - 0
MediaBrowser.Controller/Entities/BasePluginFolder.cs

@@ -11,5 +11,10 @@ namespace MediaBrowser.Controller.Entities
         {
             get { return null; }
         }
+
+        public override bool CanDelete()
+        {
+            return false;
+        }
     }
 }

+ 8 - 0
MediaBrowser.Controller/Entities/Book.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Model.Configuration;
 using System.Collections.Generic;
 using System.Linq;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities
@@ -37,6 +38,13 @@ namespace MediaBrowser.Controller.Entities
             Tags = new List<string>();
         }
 
+        public override bool CanDownload()
+        {
+            var locationType = LocationType;
+            return locationType != LocationType.Remote &&
+                   locationType != LocationType.Virtual;
+        }
+
         protected override bool GetBlockUnratedValue(UserPolicy config)
         {
             return config.BlockUnratedItems.Contains(UnratedItem.Book);

+ 10 - 9
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -35,6 +35,11 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         public string CollectionType { get; set; }
 
         /// <summary>
@@ -86,7 +91,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var path = ContainingFolderPath;
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager, directoryService)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
             {
                 FileInfo = new DirectoryInfo(path),
                 Path = path,
@@ -121,12 +126,6 @@ namespace MediaBrowser.Controller.Entities
             return args;
         }
 
-        // Cache this since it will be used a lot
-        /// <summary>
-        /// The null task result
-        /// </summary>
-        private static readonly Task NullTaskResult = Task.FromResult<object>(null);
-
         /// <summary>
         /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
         /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
@@ -138,7 +137,7 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="refreshOptions">The refresh options.</param>
         /// <param name="directoryService">The directory service.</param>
         /// <returns>Task.</returns>
-        protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
         {
             var list = PhysicalLocationsList.ToList();
 
@@ -146,8 +145,10 @@ namespace MediaBrowser.Controller.Entities
 
             if (!list.SequenceEqual(PhysicalLocationsList))
             {
-                await UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+                return UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
             }
+
+            return Task.FromResult(true);
         }
 
         /// <summary>

+ 151 - 111
MediaBrowser.Controller/Entities/Folder.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
-using MoreLinq;
 using System;
 using System.Collections;
 using System.Collections.Generic;
@@ -15,13 +14,14 @@ using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
     /// Class Folder
     /// </summary>
-    public class Folder : BaseItem, IHasThemeMedia, IHasTags
+    public class Folder : BaseItem, IHasThemeMedia, IHasTags, IHasPreferredMetadataLanguage
     {
         public static IUserManager UserManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
@@ -30,6 +30,14 @@ namespace MediaBrowser.Controller.Entities
         public List<Guid> ThemeVideoIds { get; set; }
         public List<string> Tags { get; set; }
 
+        public string PreferredMetadataLanguage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the preferred metadata country code.
+        /// </summary>
+        /// <value>The preferred metadata country code.</value>
+        public string PreferredMetadataCountryCode { get; set; }
+
         public Folder()
         {
             LinkedChildren = new List<LinkedChild>();
@@ -72,6 +80,19 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        protected override bool IsAllowTagFilterEnforced()
+        {
+            if (this is ICollectionFolder)
+            {
+                return false;
+            }
+            if (this is UserView)
+            {
+                return false;
+            }
+            return true;
+        }
+
         /// <summary>
         /// Gets or sets a value indicating whether this instance is physical root.
         /// </summary>
@@ -98,6 +119,7 @@ namespace MediaBrowser.Controller.Entities
 
         public virtual List<LinkedChild> LinkedChildren { get; set; }
 
+        [IgnoreDataMember]
         protected virtual bool SupportsShortcutChildren
         {
             get { return true; }
@@ -237,14 +259,13 @@ namespace MediaBrowser.Controller.Entities
         protected virtual IEnumerable<string> GetIndexByOptions()
         {
             return new List<string> {            
-                {LocalizedStrings.Instance.GetString("NoneDispPref")}, 
-                {LocalizedStrings.Instance.GetString("PerformerDispPref")},
-                {LocalizedStrings.Instance.GetString("GenreDispPref")},
-                {LocalizedStrings.Instance.GetString("DirectorDispPref")},
-                {LocalizedStrings.Instance.GetString("YearDispPref")},
-                {LocalizedStrings.Instance.GetString("StudioDispPref")}
+                {"None"}, 
+                {"Performer"},
+                {"Genre"},
+                {"Director"},
+                {"Year"},
+                {"Studio"}
             };
-
         }
 
         /// <summary>
@@ -275,7 +296,17 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                return _children ?? (_children = LoadChildrenInternal());
+                if (_children == null)
+                {
+                    lock (_childrenSyncLock)
+                    {
+                        if (_children == null)
+                        {
+                            _children = LoadChildrenInternal();
+                        }
+                    }
+                }
+                return _children;
             }
         }
 
@@ -301,14 +332,24 @@ namespace MediaBrowser.Controller.Entities
 
         public override bool IsVisible(User user)
         {
-            if (this is ICollectionFolder)
+            if (this is ICollectionFolder && !(this is BasePluginFolder))
             {
-                if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
+                if (user.Policy.BlockedMediaFolders != null)
+                {
+                    if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
 
-                    // Backwards compatibility
-                    user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+                        // Backwards compatibility
+                        user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
+                else
                 {
-                    return false;
+                    if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
                 }
             }
 
@@ -345,12 +386,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task.</returns>
         public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
         {
-            return ValidateChildrenWithCancellationSupport(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
-        }
-
-        private Task ValidateChildrenWithCancellationSupport(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
-        {
-            return ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+            return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
         }
 
         private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
@@ -540,50 +576,49 @@ namespace MediaBrowser.Controller.Entities
             var children = ActualChildren.ToList();
 
             var percentages = new Dictionary<Guid, double>(children.Count);
-
-            var tasks = new List<Task>();
+            var numComplete = 0;
+            var count = children.Count;
 
             foreach (var child in children)
             {
-                if (tasks.Count >= 2)
-                {
-                    await Task.WhenAll(tasks).ConfigureAwait(false);
-                    tasks.Clear();
-                }
-
                 cancellationToken.ThrowIfCancellationRequested();
-                var innerProgress = new ActionableProgress<double>();
 
-                // Avoid implicitly captured closure
-                var currentChild = child;
-                innerProgress.RegisterAction(p =>
+                if (child.IsFolder)
                 {
-                    lock (percentages)
+                    var innerProgress = new ActionableProgress<double>();
+
+                    // Avoid implicitly captured closure
+                    var currentChild = child;
+                    innerProgress.RegisterAction(p =>
                     {
-                        percentages[currentChild.Id] = p / 100;
+                        lock (percentages)
+                        {
+                            percentages[currentChild.Id] = p / 100;
 
-                        var percent = percentages.Values.Sum();
-                        percent /= children.Count;
-                        percent *= 100;
-                        progress.Report(percent);
-                    }
-                });
+                            var innerPercent = percentages.Values.Sum();
+                            innerPercent /= count;
+                            innerPercent *= 100;
+                            progress.Report(innerPercent);
+                        }
+                    });
 
-                if (child.IsFolder)
-                {
                     await RefreshChildMetadata(child, refreshOptions, recursive, innerProgress, cancellationToken)
                       .ConfigureAwait(false);
                 }
                 else
                 {
-                    // Avoid implicitly captured closure
-                    var taskChild = child;
-
-                    tasks.Add(Task.Run(async () => await RefreshChildMetadata(taskChild, refreshOptions, false, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken));
+                    await RefreshChildMetadata(child, refreshOptions, false, new Progress<double>(), cancellationToken)
+                      .ConfigureAwait(false);
                 }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= count;
+                percent *= 100;
+
+                progress.Report(percent);
             }
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
             progress.Report(100);
         }
 
@@ -648,7 +683,7 @@ namespace MediaBrowser.Controller.Entities
                     }
                 });
 
-                await child.ValidateChildrenWithCancellationSupport(innerProgress, cancellationToken, true, false, null, directoryService)
+                await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)
                         .ConfigureAwait(false);
             }
         }
@@ -678,12 +713,12 @@ namespace MediaBrowser.Controller.Entities
                 path = System.IO.Path.GetDirectoryName(path);
             }
 
-            if (ContainsPath(LibraryManager.GetDefaultVirtualFolders(), originalPath))
+            if (ContainsPath(LibraryManager.GetVirtualFolders(), originalPath))
             {
                 return true;
             }
 
-            return UserManager.Users.Any(user => ContainsPath(LibraryManager.GetVirtualFolders(user), originalPath));
+            return ContainsPath(LibraryManager.GetVirtualFolders(), originalPath);
         }
 
         /// <summary>
@@ -731,28 +766,6 @@ namespace MediaBrowser.Controller.Entities
             return childrenItems;
         }
 
-        /// <summary>
-        /// Retrieves the child.
-        /// </summary>
-        /// <param name="child">The child.</param>
-        /// <returns>BaseItem.</returns>
-        private BaseItem RetrieveChild(Guid child)
-        {
-            var item = LibraryManager.GetItemById(child);
-
-            if (item != null)
-            {
-                if (item is IByReferenceItem)
-                {
-                    return LibraryManager.GetOrAddByReferenceItem(item);
-                }
-
-                item.Parent = this;
-            }
-
-            return item;
-        }
-
         private BaseItem RetrieveChild(BaseItem child)
         {
             if (child.Id == Guid.Empty)
@@ -786,18 +799,31 @@ namespace MediaBrowser.Controller.Entities
         {
             var user = query.User;
 
-            var items = query.Recursive
-                ? GetRecursiveChildren(user)
-                : GetChildren(user, true);
+            Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+            IEnumerable<BaseItem> items;
+
+            if (query.User == null)
+            {
+                items = query.Recursive
+                   ? GetRecursiveChildren(filter)
+                   : Children.Where(filter);
+            }
+            else
+            {
+                items = query.Recursive
+                   ? GetRecursiveChildren(user, filter)
+                   : GetChildren(user, true).Where(filter);
+            }
 
-            var result = SortAndFilter(items, query);
+            var result = PostFilterAndSort(items, query);
 
             return Task.FromResult(result);
         }
 
-        protected QueryResult<BaseItem> SortAndFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
+        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
         {
-            return UserViewBuilder.SortAndFilter(items, this, null, query, LibraryManager, UserDataManager);
+            return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager);
         }
 
         /// <summary>
@@ -822,11 +848,11 @@ namespace MediaBrowser.Controller.Entities
             //the true root should return our users root folder children
             if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren);
 
-            var list = new List<BaseItem>();
+            var result = new Dictionary<Guid, BaseItem>();
 
-            var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, includeHidden, false);
+            AddChildren(user, includeLinkedChildren, result, includeHidden, false, null);
 
-            return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
+            return result.Values;
         }
 
         protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
@@ -839,31 +865,30 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <param name="user">The user.</param>
         /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
-        /// <param name="list">The list.</param>
+        /// <param name="result">The result.</param>
         /// <param name="includeHidden">if set to <c>true</c> [include hidden].</param>
         /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+        /// <param name="filter">The filter.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        private bool AddChildrenToList(User user, bool includeLinkedChildren, List<BaseItem> list, bool includeHidden, bool recursive)
+        private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool includeHidden, bool recursive, Func<BaseItem, bool> filter)
         {
-            var hasLinkedChildren = false;
-
             foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
             {
                 if (child.IsVisible(user))
                 {
                     if (includeHidden || !child.IsHiddenFromUser(user))
                     {
-                        list.Add(child);
+                        if (filter == null || filter(child))
+                        {
+                            result[child.Id] = child;
+                        }
                     }
 
                     if (recursive && child.IsFolder)
                     {
                         var folder = (Folder)child;
 
-                        if (folder.AddChildrenToList(user, includeLinkedChildren, list, includeHidden, true))
-                        {
-                            hasLinkedChildren = true;
-                        }
+                        folder.AddChildren(user, includeLinkedChildren, result, includeHidden, true, filter);
                     }
                 }
             }
@@ -874,14 +899,13 @@ namespace MediaBrowser.Controller.Entities
                 {
                     if (child.IsVisible(user))
                     {
-                        hasLinkedChildren = true;
-
-                        list.Add(child);
+                        if (filter == null || filter(child))
+                        {
+                            result[child.Id] = child;
+                        }
                     }
                 }
             }
-
-            return hasLinkedChildren;
         }
 
         /// <summary>
@@ -891,18 +915,23 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
         /// <exception cref="System.ArgumentNullException"></exception>
-        public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
+        public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
+        {
+            return GetRecursiveChildren(user, i => true);
+        }
+
+        public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, Func<BaseItem, bool> filter)
         {
             if (user == null)
             {
                 throw new ArgumentNullException("user");
             }
 
-            var list = new List<BaseItem>();
+            var result = new Dictionary<Guid, BaseItem>();
 
-            var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, false, true);
+            AddChildren(user, true, result, false, true, filter);
 
-            return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
+            return result.Values;
         }
 
         /// <summary>
@@ -910,10 +939,15 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <returns>IList{BaseItem}.</returns>
         public IList<BaseItem> GetRecursiveChildren()
+        {
+            return GetRecursiveChildren(i => true);
+        }
+
+        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
         {
             var list = new List<BaseItem>();
 
-            AddChildrenToList(list, true, null);
+            AddChildrenToList(list, true, filter);
 
             return list;
         }
@@ -1022,6 +1056,15 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => i.Item2 != null);
         }
 
+        [IgnoreDataMember]
+        protected override bool SupportsOwnedItems
+        {
+            get
+            {
+                return base.SupportsOwnedItems || SupportsShortcutChildren;
+            }
+        }
+
         protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
             var changesFound = false;
@@ -1126,8 +1169,7 @@ namespace MediaBrowser.Controller.Entities
             bool resetPosition)
         {
             // Sweep through recursively and update status
-            var tasks = GetRecursiveChildren(user, true)
-                .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
+            var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
                 .Select(c => c.MarkPlayed(user, datePlayed, resetPosition));
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -1141,8 +1183,7 @@ namespace MediaBrowser.Controller.Entities
         public override async Task MarkUnplayed(User user)
         {
             // Sweep through recursively and update status
-            var tasks = GetRecursiveChildren(user, true)
-                .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
+            var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
                 .Select(c => c.MarkUnplayed(user));
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -1171,15 +1212,15 @@ namespace MediaBrowser.Controller.Entities
                 return this;
             }
 
-            return RecursiveChildren.FirstOrDefault(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) ||
+            return GetRecursiveChildren(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) ||
                 (!i.IsFolder && !i.IsInMixedFolder && string.Equals(i.ContainingFolderPath, path, StringComparison.OrdinalIgnoreCase)) ||
-                i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
+                i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
+                .FirstOrDefault();
         }
 
         public override bool IsPlayed(User user)
         {
-            return GetRecursiveChildren(user)
-                .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
+            return GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
                 .All(i => i.IsPlayed(user));
         }
 
@@ -1206,8 +1247,7 @@ namespace MediaBrowser.Controller.Entities
             }
             else
             {
-                children = folder.GetRecursiveChildren(user)
-                    .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual);
+                children = folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual);
             }
 
             // Loop through each recursive child

+ 10 - 3
MediaBrowser.Controller/Entities/Game.cs

@@ -1,10 +1,10 @@
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -38,6 +38,13 @@ namespace MediaBrowser.Controller.Entities
         public List<Guid> LocalTrailerIds { get; set; }
         public List<Guid> RemoteTrailerIds { get; set; }
 
+        public override bool CanDownload()
+        {
+            var locationType = LocationType;
+            return locationType != LocationType.Remote &&
+                   locationType != LocationType.Virtual;
+        }
+
         /// <summary>
         /// Gets or sets the tags.
         /// </summary>
@@ -88,7 +95,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         public List<string> MultiPartGameFiles { get; set; }
 
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             var id = this.GetProviderId(MetadataProviders.Gamesdb);
 
@@ -96,7 +103,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 return "Game-Gamesdb-" + id;
             }
-            return base.GetUserDataKey();
+            return base.CreateUserDataKey();
         }
 
         public override IEnumerable<string> GetDeletePaths()

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

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -10,7 +11,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return "GameGenre-" + Name;
         }
@@ -20,6 +21,7 @@ namespace MediaBrowser.Controller.Entities
         /// If the item is a folder, it returns the folder itself
         /// </summary>
         /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
         public override string ContainingFolderPath
         {
             get
@@ -32,6 +34,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets a value indicating whether this instance is owned item.
         /// </summary>
         /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
         public override bool IsOwnedItem
         {
             get
@@ -40,9 +43,19 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         {
-            return inputItems.Where(i => (i is Game) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+            return inputItems.Where(GetItemFilter());
+        }
+
+        public Func<BaseItem, bool> GetItemFilter()
+        {
+            return i => (i is Game) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase);
         }
     }
 }

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

@@ -35,13 +35,13 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             if (!string.IsNullOrEmpty(GameSystemName))
             {
                 return "GameSystem-" + GameSystemName;
             }
-            return base.GetUserDataKey();
+            return base.CreateUserDataKey();
         }
 
         protected override bool GetBlockUnratedValue(UserPolicy config)

+ 16 - 3
MediaBrowser.Controller/Entities/Genre.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities.Audio;
+using System.Runtime.Serialization;
+using MediaBrowser.Controller.Entities.Audio;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the user data key.
         /// </summary>
         /// <returns>System.String.</returns>
-        public override string GetUserDataKey()
+        protected override string CreateUserDataKey()
         {
             return "Genre-" + Name;
         }
@@ -24,6 +25,7 @@ namespace MediaBrowser.Controller.Entities
         /// If the item is a folder, it returns the folder itself
         /// </summary>
         /// <value>The containing folder path.</value>
+        [IgnoreDataMember]
         public override string ContainingFolderPath
         {
             get
@@ -32,10 +34,16 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public override bool CanDelete()
+        {
+            return false;
+        }
+
         /// <summary>
         /// Gets a value indicating whether this instance is owned item.
         /// </summary>
         /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
         public override bool IsOwnedItem
         {
             get
@@ -46,7 +54,12 @@ namespace MediaBrowser.Controller.Entities
 
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         {
-            return inputItems.Where(i => !(i is Game) && !(i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase));
+            return inputItems.Where(GetItemFilter());
+        }
+
+        public Func<BaseItem, bool> GetItemFilter()
+        {
+            return i => !(i is Game) && !(i is IHasMusicGenres) && i.Genres.Contains(Name, StringComparer.OrdinalIgnoreCase);
         }
     }
 }

+ 5 - 0
MediaBrowser.Controller/Entities/IHasMetadata.cs

@@ -54,5 +54,10 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the item identities.
         /// </summary>
         List<IItemIdentity> Identities { get; set; }
+
+        /// <summary>
+        /// Afters the metadata refresh.
+        /// </summary>
+        void AfterMetadataRefresh();
     }
 }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.