Browse Source

Merge pull request #1 from MediaBrowser/master

Update to latest
7illusions 10 years ago
parent
commit
66ad1699e2
100 changed files with 3471 additions and 2670 deletions
  1. 66 16
      MediaBrowser.Api/ApiEntryPoint.cs
  2. 3 1
      MediaBrowser.Api/AppThemeService.cs
  3. 0 190
      MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs
  4. 27 30
      MediaBrowser.Api/BaseApiService.cs
  5. 28 0
      MediaBrowser.Api/BrandingService.cs
  6. 17 14
      MediaBrowser.Api/ChannelService.cs
  7. 95 70
      MediaBrowser.Api/ConfigurationService.cs
  8. 0 671
      MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs
  9. 0 83
      MediaBrowser.Api/DefaultTheme/Models.cs
  10. 3 1
      MediaBrowser.Api/DisplayPreferencesService.cs
  11. 11 6
      MediaBrowser.Api/Dlna/DlnaServerService.cs
  12. 2 0
      MediaBrowser.Api/Dlna/DlnaService.cs
  13. 2 0
      MediaBrowser.Api/EnvironmentService.cs
  14. 2 0
      MediaBrowser.Api/GamesService.cs
  15. 6 2
      MediaBrowser.Api/Images/ImageByNameService.cs
  16. 84 58
      MediaBrowser.Api/Images/ImageService.cs
  17. 0 96
      MediaBrowser.Api/Images/ImageWriter.cs
  18. 10 19
      MediaBrowser.Api/Images/RemoteImageService.cs
  19. 25 6
      MediaBrowser.Api/ItemLookupService.cs
  20. 17 8
      MediaBrowser.Api/ItemRefreshService.cs
  21. 18 5
      MediaBrowser.Api/ItemUpdateService.cs
  22. 2 0
      MediaBrowser.Api/Library/ChapterService.cs
  23. 2 0
      MediaBrowser.Api/Library/FileOrganizationService.cs
  24. 1 1
      MediaBrowser.Api/Library/LibraryHelpers.cs
  25. 20 16
      MediaBrowser.Api/Library/LibraryService.cs
  26. 2 26
      MediaBrowser.Api/Library/LibraryStructureService.cs
  27. 42 40
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  28. 2 0
      MediaBrowser.Api/LocalizationService.cs
  29. 16 13
      MediaBrowser.Api/MediaBrowser.Api.csproj
  30. 6 9
      MediaBrowser.Api/Movies/CollectionService.cs
  31. 2 0
      MediaBrowser.Api/Movies/MoviesService.cs
  32. 2 0
      MediaBrowser.Api/Movies/TrailersService.cs
  33. 2 1
      MediaBrowser.Api/Music/AlbumsService.cs
  34. 38 0
      MediaBrowser.Api/Music/InstantMixService.cs
  35. 2 0
      MediaBrowser.Api/NotificationsService.cs
  36. 2 0
      MediaBrowser.Api/PackageReviewService.cs
  37. 2 0
      MediaBrowser.Api/PackageService.cs
  38. 185 112
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  39. 186 0
      MediaBrowser.Api/Playback/BifService.cs
  40. 0 32
      MediaBrowser.Api/Playback/EndlessStreamCopy.cs
  41. 68 84
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  42. 400 74
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  43. 3 1
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  44. 16 7
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  45. 80 51
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  46. 26 14
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  47. 5 5
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  48. 8 2
      MediaBrowser.Api/Playback/StreamRequest.cs
  49. 9 0
      MediaBrowser.Api/Playback/StreamState.cs
  50. 176 0
      MediaBrowser.Api/PlaylistService.cs
  51. 2 0
      MediaBrowser.Api/PluginService.cs
  52. 2 0
      MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
  53. 6 4
      MediaBrowser.Api/SearchService.cs
  54. 1 1
      MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs
  55. 96 22
      MediaBrowser.Api/Session/SessionsService.cs
  56. 2 2
      MediaBrowser.Api/SimilarItemsHelper.cs
  57. 116 31
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  58. 104 0
      MediaBrowser.Api/Sync/SyncService.cs
  59. 53 0
      MediaBrowser.Api/System/ActivityLogService.cs
  60. 67 0
      MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
  61. 1 1
      MediaBrowser.Api/System/SystemInfoWebSocketListener.cs
  62. 205 0
      MediaBrowser.Api/System/SystemService.cs
  63. 0 90
      MediaBrowser.Api/SystemService.cs
  64. 2 0
      MediaBrowser.Api/TvShowsService.cs
  65. 4 2
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  66. 3 3
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  67. 4 2
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  68. 4 2
      MediaBrowser.Api/UserLibrary/GenresService.cs
  69. 3 1
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  70. 4 2
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  71. 4 2
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  72. 388 0
      MediaBrowser.Api/UserLibrary/PlaystateService.cs
  73. 4 2
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  74. 137 384
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  75. 4 2
      MediaBrowser.Api/UserLibrary/YearsService.cs
  76. 112 78
      MediaBrowser.Api/UserService.cs
  77. 10 8
      MediaBrowser.Api/VideosService.cs
  78. 0 149
      MediaBrowser.Api/WebSocket/LogFileWebSocketListener.cs
  79. 48 8
      MediaBrowser.Common.Implementations/BaseApplicationHost.cs
  80. 94 9
      MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs
  81. 5 17
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  82. 15 0
      MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs
  83. 7 7
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  84. 85 2
      MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs
  85. 1 0
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  86. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  87. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  88. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  89. 1 1
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/SystemUpdateTask.cs
  90. 1 4
      MediaBrowser.Common.Implementations/Security/UsageReporter.cs
  91. 0 20
      MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs
  92. 0 15
      MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs
  93. 34 14
      MediaBrowser.Common.Implementations/Updates/InstallationManager.cs
  94. 2 2
      MediaBrowser.Common.Implementations/packages.config
  95. 16 12
      MediaBrowser.Common/Configuration/ConfigurationHelper.cs
  96. 18 0
      MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
  97. 17 0
      MediaBrowser.Common/Configuration/IConfigurationFactory.cs
  98. 47 1
      MediaBrowser.Common/Configuration/IConfigurationManager.cs
  99. 14 0
      MediaBrowser.Common/IO/IFileSystem.cs
  100. 4 3
      MediaBrowser.Common/MediaBrowser.Common.csproj

+ 66 - 16
MediaBrowser.Api/ApiEntryPoint.cs

@@ -37,11 +37,14 @@ namespace MediaBrowser.Api
 
         private readonly ISessionManager _sessionManager;
 
+        public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="appPaths">The application paths.</param>
+        /// <param name="sessionManager">The session manager.</param>
         public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths, ISessionManager sessionManager)
         {
             Logger = logger;
@@ -99,7 +102,7 @@ namespace MediaBrowser.Api
         {
             var jobCount = _activeTranscodingJobs.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, true));
+            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, path => true));
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
@@ -119,14 +122,12 @@ namespace MediaBrowser.Api
         /// <param name="path">The path.</param>
         /// <param name="type">The type.</param>
         /// <param name="process">The process.</param>
-        /// <param name="startTimeTicks">The start time ticks.</param>
         /// <param name="deviceId">The device id.</param>
         /// <param name="state">The state.</param>
         /// <param name="cancellationTokenSource">The cancellation token source.</param>
         public void OnTranscodeBeginning(string path,
             TranscodingJobType type,
             Process process,
-            long? startTimeTicks,
             string deviceId,
             StreamState state,
             CancellationTokenSource cancellationTokenSource)
@@ -139,7 +140,6 @@ namespace MediaBrowser.Api
                     Path = path,
                     Process = process,
                     ActiveRequestCount = 1,
-                    StartTimeTicks = startTimeTicks,
                     DeviceId = deviceId,
                     CancellationTokenSource = cancellationTokenSource
                 });
@@ -214,10 +214,15 @@ namespace MediaBrowser.Api
         /// <param name="type">The type.</param>
         /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
         public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
+        {
+            return GetTranscodingJob(path, type) != null;
+        }
+
+        public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
         {
             lock (_activeTranscodingJobs)
             {
-                return _activeTranscodingJobs.Any(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
             }
         }
 
@@ -290,34 +295,70 @@ namespace MediaBrowser.Api
         {
             var job = (TranscodingJob)state;
 
-            KillTranscodingJob(job, true);
+            KillTranscodingJob(job, path => true);
         }
 
         /// <summary>
         /// Kills the single transcoding job.
         /// </summary>
         /// <param name="deviceId">The device id.</param>
-        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
+        /// <param name="deleteFiles">The delete files.</param>
+        /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="ArgumentNullException">deviceId</exception>
         /// <exception cref="System.ArgumentNullException">sourcePath</exception>
-        internal void KillTranscodingJobs(string deviceId, bool deleteFiles)
+        internal Task KillTranscodingJobs(string deviceId, Func<string, bool> deleteFiles, bool acquireLock)
         {
             if (string.IsNullOrEmpty(deviceId))
             {
                 throw new ArgumentNullException("deviceId");
             }
 
+            return KillTranscodingJobs(j => string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase), deleteFiles, acquireLock);
+        }
+
+        /// <summary>
+        /// Kills the transcoding jobs.
+        /// </summary>
+        /// <param name="killJob">The kill job.</param>
+        /// <param name="deleteFiles">The delete files.</param>
+        /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">deviceId</exception>
+        internal async Task KillTranscodingJobs(Func<TranscodingJob,bool> killJob, Func<string, bool> deleteFiles, bool acquireLock)
+        {
             var jobs = new List<TranscodingJob>();
 
             lock (_activeTranscodingJobs)
             {
                 // This is really only needed for HLS. 
                 // Progressive streams can stop on their own reliably
-                jobs.AddRange(_activeTranscodingJobs.Where(i => string.Equals(deviceId, i.DeviceId, StringComparison.OrdinalIgnoreCase)));
+                jobs.AddRange(_activeTranscodingJobs.Where(killJob));
+            }
+
+            if (jobs.Count == 0)
+            {
+                return;
+            }
+
+            if (acquireLock)
+            {
+                await TranscodingStartLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
             }
 
-            foreach (var job in jobs)
+            try
+            {
+                foreach (var job in jobs)
+                {
+                    KillTranscodingJob(job, deleteFiles);
+                }
+            }
+            finally
             {
-                KillTranscodingJob(job, deleteFiles);
+                if (acquireLock)
+                {
+                    TranscodingStartLock.Release();
+                }
             }
         }
 
@@ -325,8 +366,8 @@ namespace MediaBrowser.Api
         /// Kills the transcoding job.
         /// </summary>
         /// <param name="job">The job.</param>
-        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
-        private void KillTranscodingJob(TranscodingJob job, bool deleteFiles)
+        /// <param name="delete">The delete.</param>
+        private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
         {
             lock (_activeTranscodingJobs)
             {
@@ -378,7 +419,7 @@ namespace MediaBrowser.Api
                 }
             }
 
-            if (deleteFiles)
+            if (delete(job.Path))
             {
                 DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
             }
@@ -386,7 +427,7 @@ namespace MediaBrowser.Api
 
         private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
         {
-            if (retryCount >= 8)
+            if (retryCount >= 10)
             {
                 return;
             }
@@ -440,6 +481,8 @@ namespace MediaBrowser.Api
                 .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1)
                 .ToList();
 
+            Exception e = null;
+
             foreach (var file in filesToDelete)
             {
                 try
@@ -449,9 +492,15 @@ namespace MediaBrowser.Api
                 }
                 catch (IOException ex)
                 {
+                    e = ex;
                     Logger.ErrorException("Error deleting HLS file {0}", ex, file);
                 }
             }
+
+            if (e != null)
+            {
+                throw e;
+            }
         }
     }
 
@@ -486,12 +535,13 @@ namespace MediaBrowser.Api
         /// <value>The kill timer.</value>
         public Timer KillTimer { get; set; }
 
-        public long? StartTimeTicks { get; set; }
         public string DeviceId { get; set; }
 
         public CancellationTokenSource CancellationTokenSource { get; set; }
 
         public object ProcessLock = new object();
+
+        public bool HasExited { get; set; }
     }
 
     /// <summary>

+ 3 - 1
MediaBrowser.Api/AppThemeService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Themes;
 using MediaBrowser.Model.Themes;
 using ServiceStack;
@@ -47,6 +48,7 @@ namespace MediaBrowser.Api
     {
     }
 
+    [Authenticated]
     public class AppThemeService : BaseApiService
     {
         private readonly IAppThemeManager _themeManager;
@@ -92,7 +94,7 @@ namespace MediaBrowser.Api
 
             var contentType = MimeTypes.GetMimeType(info.Path);
 
-            return ToCachedResult(cacheGuid, info.DateModified, cacheDuration, () => _fileSystem.GetFileStream(info.Path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType);
+            return ResultFactory.GetCachedResult(Request, cacheGuid, null, cacheDuration, () => _fileSystem.GetFileStream(info.Path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType);
         }
     }
 }

+ 0 - 190
MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs

@@ -1,190 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Logging;
-using ServiceStack.Web;
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Api
-{
-    public class AuthorizationRequestFilterAttribute : Attribute, IHasRequestFilter
-    {
-        //This property will be resolved by the IoC container
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
-
-        public ISessionManager SessionManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        public ILogger Logger { get; set; }
-
-        /// <summary>
-        /// The request filter is executed before the service.
-        /// </summary>
-        /// <param name="request">The http request wrapper</param>
-        /// <param name="response">The http response wrapper</param>
-        /// <param name="requestDto">The request DTO</param>
-        public void RequestFilter(IRequest request, IResponse response, object requestDto)
-        {
-            //This code is executed before the service
-            var auth = GetAuthorizationDictionary(request);
-
-            if (auth != null)
-            {
-                User user = null;
-
-                if (auth.ContainsKey("UserId"))
-                {
-                    var userId = auth["UserId"];
-
-                    if (!string.IsNullOrEmpty(userId))
-                    {
-                        user = UserManager.GetUserById(new Guid(userId));
-                    }
-                }
-
-                string deviceId;
-                string device;
-                string client;
-                string version;
-
-                auth.TryGetValue("DeviceId", out deviceId);
-                auth.TryGetValue("Device", out device);
-                auth.TryGetValue("Client", out client);
-                auth.TryGetValue("Version", out version);
-
-                if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version))
-                {
-                    var remoteEndPoint = request.RemoteIp;
-
-                    SessionManager.LogSessionActivity(client, version, deviceId, device, remoteEndPoint, user);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the auth.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private static Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
-        {
-            var auth = httpReq.Headers["Authorization"];
-
-            return GetAuthorization(auth);
-        }
-
-        public static User GetCurrentUser(IRequest httpReq, IUserManager userManager)
-        {
-            var info = GetAuthorization(httpReq);
-
-            return string.IsNullOrEmpty(info.UserId) ? null : 
-                userManager.GetUserById(new Guid(info.UserId));
-        }
-
-        /// <summary>
-        /// Gets the authorization.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        public static AuthorizationInfo GetAuthorization(IRequest httpReq)
-        {
-            var auth = GetAuthorizationDictionary(httpReq);
-
-            string userId = null;
-            string deviceId = null;
-            string device = null;
-            string client = null;
-            string version = null;
-
-            if (auth != null)
-            {
-                auth.TryGetValue("UserId", out userId);
-                auth.TryGetValue("DeviceId", out deviceId);
-                auth.TryGetValue("Device", out device);
-                auth.TryGetValue("Client", out client);
-                auth.TryGetValue("Version", out version);
-            }
-
-            return new AuthorizationInfo
-            {
-                Client = client,
-                Device = device,
-                DeviceId = deviceId,
-                UserId = userId,
-                Version = version
-            };
-        }
-
-        /// <summary>
-        /// Gets the authorization.
-        /// </summary>
-        /// <param name="authorizationHeader">The authorization header.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private static Dictionary<string, string> GetAuthorization(string authorizationHeader)
-        {
-            if (authorizationHeader == null) return null;
-
-            var parts = authorizationHeader.Split(' ');
-
-            // There should be at least to parts
-            if (parts.Length < 2) return null;
-
-            // It has to be a digest request
-            if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase))
-            {
-                return null;
-            }
-
-            // Remove uptil the first space
-            authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' '));
-            parts = authorizationHeader.Split(',');
-
-            var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            foreach (var item in parts)
-            {
-                var param = item.Trim().Split(new[] { '=' }, 2);
-                result.Add(param[0], param[1].Trim(new[] { '"' }));
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// A new shallow copy of this filter is used on every request.
-        /// </summary>
-        /// <returns>IHasRequestFilter.</returns>
-        public IHasRequestFilter Copy()
-        {
-            return this;
-        }
-
-        /// <summary>
-        /// Order in which Request Filters are executed.
-        /// &lt;0 Executed before global request filters
-        /// &gt;0 Executed after global request filters
-        /// </summary>
-        /// <value>The priority.</value>
-        public int Priority
-        {
-            get { return 0; }
-        }
-    }
-
-    public class AuthorizationInfo
-    {
-        public string UserId;
-        public string DeviceId;
-        public string Device;
-        public string Client;
-        public string Version;
-    }
-}

+ 27 - 30
MediaBrowser.Api/BaseApiService.cs

@@ -14,8 +14,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class BaseApiService
     /// </summary>
-    [AuthorizationRequestFilter]
-    public class BaseApiService : IHasResultFactory, IRestfulService
+    public class BaseApiService : IHasResultFactory, IRestfulService, IHasSession
     {
         /// <summary>
         /// Gets or sets the logger.
@@ -35,6 +34,8 @@ namespace MediaBrowser.Api
         /// <value>The request context.</value>
         public IRequest Request { get; set; }
 
+        public ISessionContext SessionContext { get; set; }
+
         public string GetHeader(string name)
         {
             return Request.Headers[name];
@@ -82,33 +83,18 @@ namespace MediaBrowser.Api
         /// <summary>
         /// Gets the session.
         /// </summary>
-        /// <param name="sessionManager">The session manager.</param>
         /// <returns>SessionInfo.</returns>
-        protected SessionInfo GetSession(ISessionManager sessionManager)
+        /// <exception cref="System.ArgumentException">Session not found.</exception>
+        protected SessionInfo GetSession()
         {
-            var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
+            var session = SessionContext.GetSession(Request);
 
-            return sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) &&
-                string.Equals(i.Client, auth.Client) &&
-                string.Equals(i.ApplicationVersion, auth.Version));
-        }
+            if (session == null)
+            {
+                throw new ArgumentException("Session not found.");
+            }
 
-        /// <summary>
-        /// To the cached result.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="cacheKey">The cache key.</param>
-        /// <param name="lastDateModified">The last date modified.</param>
-        /// <param name="cacheDuration">Duration of the cache.</param>
-        /// <param name="factoryFn">The factory fn.</param>
-        /// <param name="contentType">Type of the content.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <returns>System.Object.</returns>
-        /// <exception cref="System.ArgumentNullException">cacheKey</exception>
-        protected object ToCachedResult<T>(Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType, IDictionary<string,string> responseHeaders = null)
-          where T : class
-        {
-            return ResultFactory.GetCachedResult(Request, cacheKey, lastDateModified, cacheDuration, factoryFn, contentType, responseHeaders);
+            return session;
         }
 
         /// <summary>
@@ -121,7 +107,7 @@ namespace MediaBrowser.Api
             return ResultFactory.GetStaticFileResult(Request, path);
         }
 
-        private readonly char[] _dashReplaceChars = new[] { '?', '/' };
+        private readonly char[] _dashReplaceChars = { '?', '/' };
         private const char SlugChar = '-';
 
         protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
@@ -154,7 +140,7 @@ namespace MediaBrowser.Api
             return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
         }
 
-        protected IList<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
+        protected IEnumerable<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
         {
             if (!string.IsNullOrEmpty(parentId))
             {
@@ -164,7 +150,12 @@ namespace MediaBrowser.Api
                 {
                     var user = userManager.GetUserById(userId.Value);
 
-                    return folder.GetRecursiveChildren(user).ToList();
+                    if (user == null)
+                    {
+                        throw new ArgumentException("User not found");
+                    }
+
+                    return folder.GetRecursiveChildren(user);
                 }
 
                 return folder.GetRecursiveChildren();
@@ -173,7 +164,12 @@ namespace MediaBrowser.Api
             {
                 var user = userManager.GetUserById(userId.Value);
 
-                return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user, null);
+                if (user == null)
+                {
+                    throw new ArgumentException("User not found");
+                }
+
+                return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
             }
 
             return libraryManager.RootFolder.GetRecursiveChildren();
@@ -234,7 +230,8 @@ namespace MediaBrowser.Api
                 return name;
             }
 
-            return libraryManager.RootFolder.GetRecursiveChildren(i => i is Game)
+            return libraryManager.RootFolder.GetRecursiveChildren()
+                .OfType<Game>()
                 .SelectMany(i => i.Genres)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .FirstOrDefault(i =>

+ 28 - 0
MediaBrowser.Api/BrandingService.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Branding;
+using ServiceStack;
+
+namespace MediaBrowser.Api
+{
+    [Route("/Branding/Configuration", "GET", Summary = "Gets branding configuration")]
+    public class GetBrandingOptions : IReturn<BrandingOptions>
+    {
+    }
+    
+    public class BrandingService : BaseApiService
+    {
+        private readonly IConfigurationManager _config;
+
+        public BrandingService(IConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public object Get(GetBrandingOptions request)
+        {
+            var result = _config.GetConfiguration<BrandingOptions>("branding");
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 17 - 14
MediaBrowser.Api/ChannelService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -8,6 +9,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Api
 {
@@ -172,7 +174,8 @@ namespace MediaBrowser.Api
         [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string UserId { get; set; }
     }
-    
+
+    [Authenticated]
     public class ChannelService : BaseApiService
     {
         private readonly IChannelManager _channelManager;
@@ -196,14 +199,14 @@ namespace MediaBrowser.Api
             return ToOptimizedResult(result);
         }
 
-        public object Get(GetChannelFolder request)
+        public async Task<object> Get(GetChannelFolder request)
         {
-            return ToOptimizedResult(_channelManager.GetChannelFolder(request.UserId, CancellationToken.None).Result);
+            return ToOptimizedResult(await _channelManager.GetChannelFolder(request.UserId, CancellationToken.None).ConfigureAwait(false));
         }
-        
-        public object Get(GetChannels request)
+
+        public async Task<object> Get(GetChannels request)
         {
-            var result = _channelManager.GetChannels(new ChannelQuery
+            var result = await _channelManager.GetChannels(new ChannelQuery
             {
                 Limit = request.Limit,
                 StartIndex = request.StartIndex,
@@ -211,14 +214,14 @@ namespace MediaBrowser.Api
                 SupportsLatestItems = request.SupportsLatestItems,
                 IsFavorite = request.IsFavorite
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedResult(result);
         }
 
-        public object Get(GetChannelItems request)
+        public async Task<object> Get(GetChannelItems request)
         {
-            var result = _channelManager.GetChannelItems(new ChannelItemQuery
+            var result = await _channelManager.GetChannelItems(new ChannelItemQuery
             {
                 Limit = request.Limit,
                 StartIndex = request.StartIndex,
@@ -228,16 +231,16 @@ namespace MediaBrowser.Api
                 SortOrder = request.SortOrder,
                 SortBy = (request.SortBy ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
                 Filters = request.GetFilters().ToArray(),
-                Fields = request.GetItemFields().ToList()
+                Fields = request.GetItemFields().ToArray()
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedResult(result);
         }
 
-        public object Get(GetLatestChannelItems request)
+        public async Task<object> Get(GetLatestChannelItems request)
         {
-            var result = _channelManager.GetLatestChannelItems(new AllChannelMediaQuery
+            var result = await _channelManager.GetLatestChannelItems(new AllChannelMediaQuery
             {
                 Limit = request.Limit,
                 StartIndex = request.StartIndex,
@@ -246,7 +249,7 @@ namespace MediaBrowser.Api
                 Filters = request.GetFilters().ToArray(),
                 Fields = request.GetItemFields().ToList()
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedResult(result);
         }

+ 95 - 70
MediaBrowser.Api/ConfigurationService.cs

@@ -1,15 +1,17 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 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;
 
 namespace MediaBrowser.Api
@@ -18,35 +20,58 @@ namespace MediaBrowser.Api
     /// Class GetConfiguration
     /// </summary>
     [Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
+    [Authenticated]
     public class GetConfiguration : IReturn<ServerConfiguration>
     {
 
     }
 
+    [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")]
+    [Authenticated]
+    public class GetNamedConfiguration
+    {
+        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Key { get; set; }
+    }
+    
     /// <summary>
     /// Class UpdateConfiguration
     /// </summary>
     [Route("/System/Configuration", "POST", Summary = "Updates application configuration")]
+    [Authenticated]
     public class UpdateConfiguration : ServerConfiguration, IReturnVoid
     {
     }
 
+    [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")]
+    [Authenticated]
+    public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream
+    {
+        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Key { get; set; }
+
+        public Stream RequestStream { get; set; }
+    }
+    
     [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")]
+    [Authenticated]
     public class GetDefaultMetadataOptions : IReturn<MetadataOptions>
     {
 
     }
 
     [Route("/System/Configuration/MetadataPlugins", "GET", Summary = "Gets all available metadata plugins")]
+    [Authenticated]
     public class GetMetadataPlugins : IReturn<List<MetadataPluginSummary>>
     {
 
     }
 
-    [Route("/System/Configuration/VideoImageExtraction", "POST", Summary = "Updates image extraction for all types")]
-    public class UpdateVideoImageExtraction : IReturnVoid
+    [Route("/System/Configuration/MetadataPlugins/Autoset", "POST")]
+    [Authenticated]
+    public class AutoSetMetadataOptions : IReturnVoid
     {
-        public bool Enabled { get; set; }
+
     }
 
     public class ConfigurationService : BaseApiService
@@ -63,13 +88,15 @@ namespace MediaBrowser.Api
 
         private readonly IFileSystem _fileSystem;
         private readonly IProviderManager _providerManager;
+        private readonly ILibraryManager _libraryManager;
 
-        public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager)
+        public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager)
         {
             _jsonSerializer = jsonSerializer;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
             _providerManager = providerManager;
+            _libraryManager = libraryManager;
         }
 
         /// <summary>
@@ -88,95 +115,93 @@ namespace MediaBrowser.Api
             return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => _configurationManager.Configuration);
         }
 
-        /// <summary>
-        /// Posts the specified configuraiton.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateConfiguration request)
+        public object Get(GetNamedConfiguration request)
         {
-            // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
+            var result = _configurationManager.GetConfiguration(request.Key);
 
-            var json = _jsonSerializer.SerializeToString(request);
-
-            var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
-
-            _configurationManager.ReplaceConfiguration(config);
+            return ToOptimizedResult(result);
         }
 
-        public object Get(GetDefaultMetadataOptions request)
-        {
-            return ToOptimizedSerializedResultUsingCache(new MetadataOptions());
-        }
+        const string XbmcMetadata = "Xbmc Nfo";
+        const string MediaBrowserMetadata = "Media Browser Xml";
 
-        public object Get(GetMetadataPlugins request)
+        public void Post(AutoSetMetadataOptions request)
         {
-            return ToOptimizedSerializedResultUsingCache(_providerManager.GetAllMetadataPlugins().ToList());
-        }
+            var service = AutoDetectMetadataService();
 
-        /// <summary>
-        /// This is a temporary method used until image settings get broken out.
-        /// </summary>
-        /// <param name="request"></param>
-        public void Post(UpdateVideoImageExtraction request)
-        {
-            var config = _configurationManager.Configuration;
+            Logger.Info("Setting preferred metadata format to " + service);
 
-            EnableImageExtractionForType(typeof(Movie), config, request.Enabled);
-            EnableImageExtractionForType(typeof(Episode), config, request.Enabled);
-            EnableImageExtractionForType(typeof(AdultVideo), config, request.Enabled);
-            EnableImageExtractionForType(typeof(MusicVideo), config, request.Enabled);
-            EnableImageExtractionForType(typeof(Video), config, request.Enabled);
-            EnableImageExtractionForType(typeof(Trailer), config, request.Enabled);
+            var serviceToDisable = string.Equals(service, XbmcMetadata) ?
+                MediaBrowserMetadata :
+                XbmcMetadata;
 
+            _configurationManager.DisableMetadataService(serviceToDisable);
             _configurationManager.SaveConfiguration();
         }
 
-        private void EnableImageExtractionForType(Type type, ServerConfiguration config, bool enabled)
+        private string AutoDetectMetadataService()
         {
-            var options = GetMetadataOptions(type, config);
-
-            const string imageProviderName = "Screen Grabber";
-
-            var contains = options.DisabledImageFetchers.Contains(imageProviderName, StringComparer.OrdinalIgnoreCase);
-
-            if (!enabled && !contains)
+            try
             {
-                var list = options.DisabledImageFetchers.ToList();
-
-                list.Add(imageProviderName);
+                var paths = _libraryManager.GetDefaultVirtualFolders()
+                   .SelectMany(i => i.Locations)
+                   .Distinct(StringComparer.OrdinalIgnoreCase)
+                   .Select(i => new DirectoryInfo(i))
+                   .ToList();
+
+                if (paths.SelectMany(i => i.EnumerateFiles("*.xml", SearchOption.AllDirectories))
+                    .Any())
+                {
+                    return XbmcMetadata;
+                }
 
-                options.DisabledImageFetchers = list.ToArray();
+                if (paths.SelectMany(i => i.EnumerateFiles("*.xml", SearchOption.AllDirectories))
+                    .Any(i => string.Equals(i.Name, "series.xml", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "movie.xml", StringComparison.OrdinalIgnoreCase)))
+                {
+                    return MediaBrowserMetadata;
+                }
             }
-            else if (enabled && contains)
+            catch (Exception)
             {
-                var list = options.DisabledImageFetchers.ToList();
-
-                list.Remove(imageProviderName);
-
-                options.DisabledImageFetchers = list.ToArray();
+                
             }
+            
+            return XbmcMetadata;
         }
 
-        private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config)
+        /// <summary>
+        /// Posts the specified configuraiton.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateConfiguration request)
         {
-            var options = config.MetadataOptions
-                .FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase));
+            // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
+            var json = _jsonSerializer.SerializeToString(request);
 
-            if (options == null)
-            {
-                var list = config.MetadataOptions.ToList();
+            var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
 
-                options = new MetadataOptions
-                {
-                    ItemType = type.Name
-                };
+            _configurationManager.ReplaceConfiguration(config);
+        }
 
-                list.Add(options);
+        public void Post(UpdateNamedConfiguration request)
+        {
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var key = pathInfo.GetArgumentValue<string>(2);
 
-                config.MetadataOptions = list.ToArray();
-            }
+            var configurationType = _configurationManager.GetConfigurationType(key);
+            var configuration = _jsonSerializer.DeserializeFromStream(request.RequestStream, configurationType);
+            
+            _configurationManager.SaveConfiguration(key, configuration);
+        }
+
+        public object Get(GetDefaultMetadataOptions request)
+        {
+            return ToOptimizedSerializedResultUsingCache(new MetadataOptions());
+        }
 
-            return options;
+        public object Get(GetMetadataPlugins request)
+        {
+            return ToOptimizedSerializedResultUsingCache(_providerManager.GetAllMetadataPlugins().ToList());
         }
     }
 }

+ 0 - 671
MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs

@@ -1,671 +0,0 @@
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Querying;
-using ServiceStack;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MediaBrowser.Api.DefaultTheme
-{
-    [Route("/MBT/DefaultTheme/Games", "GET")]
-    public class GetGamesView : IReturn<GamesView>
-    {
-        [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "RecentlyPlayedGamesLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int RecentlyPlayedGamesLimit { get; set; }
-
-        public string ParentId { get; set; }
-    }
-
-    [Route("/MBT/DefaultTheme/TV", "GET")]
-    public class GetTvView : IReturn<TvView>
-    {
-        [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ComedyGenre { get; set; }
-
-        [ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string RomanceGenre { get; set; }
-
-        [ApiMember(Name = "TopCommunityRating", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double TopCommunityRating { get; set; }
-
-        [ApiMember(Name = "NextUpEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int NextUpEpisodeLimit { get; set; }
-
-        [ApiMember(Name = "ResumableEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int ResumableEpisodeLimit { get; set; }
-
-        [ApiMember(Name = "LatestEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int LatestEpisodeLimit { get; set; }
-
-        public string ParentId { get; set; }
-    }
-
-    [Route("/MBT/DefaultTheme/Movies", "GET")]
-    public class GetMovieView : IReturn<MoviesView>
-    {
-        [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "FamilyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string FamilyGenre { get; set; }
-
-        [ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ComedyGenre { get; set; }
-
-        [ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string RomanceGenre { get; set; }
-
-        [ApiMember(Name = "LatestMoviesLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int LatestMoviesLimit { get; set; }
-
-        [ApiMember(Name = "LatestTrailersLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int LatestTrailersLimit { get; set; }
-
-        public string ParentId { get; set; }
-    }
-
-    [Route("/MBT/DefaultTheme/Favorites", "GET")]
-    public class GetFavoritesView : IReturn<FavoritesView>
-    {
-        [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    public class DefaultThemeService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IDtoService _dtoService;
-        private readonly ILogger _logger;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserDataManager _userDataManager;
-
-        private readonly IImageProcessor _imageProcessor;
-        private readonly IItemRepository _itemRepo;
-
-        public DefaultThemeService(IUserManager userManager, IDtoService dtoService, ILogger logger, ILibraryManager libraryManager, IImageProcessor imageProcessor, IUserDataManager userDataManager, IItemRepository itemRepo)
-        {
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _logger = logger;
-            _libraryManager = libraryManager;
-            _imageProcessor = imageProcessor;
-            _userDataManager = userDataManager;
-            _itemRepo = itemRepo;
-        }
-
-        public object Get(GetFavoritesView request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var allItems = user.RootFolder.GetRecursiveChildren(user)
-                .ToList();
-
-            var allFavoriteItems = allItems.Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite)
-                .ToList();
-
-            var itemsWithImages = allFavoriteItems.Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath))
-                .ToList();
-
-            var itemsWithBackdrops = allFavoriteItems.Where(i => i.GetImages(ImageType.Backdrop).Any())
-                .ToList();
-
-            var view = new FavoritesView();
-
-            var fields = new List<ItemFields>();
-
-            view.BackdropItems = FilterItemsForBackdropDisplay(itemsWithBackdrops)
-                .Randomize("backdrop")
-                .Take(10)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            var spotlightItems = itemsWithBackdrops.Randomize("spotlight")
-                                                   .Take(10)
-                                                   .ToList();
-
-            view.SpotlightItems = spotlightItems
-              .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-              .ToList();
-
-            fields.Add(ItemFields.PrimaryImageAspectRatio);
-
-            view.Albums = itemsWithImages
-                .OfType<MusicAlbum>()
-                .Randomize()
-                .Take(4)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Books = itemsWithImages
-                .OfType<Book>()
-                .Randomize()
-                .Take(6)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Episodes = itemsWithImages
-                .OfType<Episode>()
-                .Randomize()
-                .Take(6)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Games = itemsWithImages
-                .OfType<Game>()
-                .Randomize()
-                .Take(6)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Movies = itemsWithImages
-                .OfType<Movie>()
-                .Randomize()
-                .Take(6)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Series = itemsWithImages
-                .OfType<Series>()
-                .Randomize()
-                .Take(6)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.Songs = itemsWithImages
-                .OfType<Audio>()
-                .Randomize()
-                .Take(4)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.MiniSpotlights = itemsWithBackdrops
-                .Except(spotlightItems)
-                .Randomize()
-                .Take(5)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            var artists = allItems.OfType<Audio>()
-                .SelectMany(i => i.AllArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Randomize()
-            .Select(i =>
-            {
-                try
-                {
-                    return _libraryManager.GetArtist(i);
-                }
-                catch
-                {
-                    return null;
-                }
-            })
-                .Where(i => i != null && _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite)
-                .Take(4)
-                .ToList();
-
-            view.Artists = artists
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            return ToOptimizedSerializedResultUsingCache(view);
-        }
-
-        public object Get(GetGamesView request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var items = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId).Where(i => i is Game || i is GameSystem)
-                .ToList();
-
-            var gamesWithImages = items.OfType<Game>().Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath)).ToList();
-
-            var itemsWithBackdrops = FilterItemsForBackdropDisplay(items.Where(i => i.GetImages(ImageType.Backdrop).Any())).ToList();
-
-            var gamesWithBackdrops = itemsWithBackdrops.OfType<Game>().ToList();
-
-            var view = new GamesView();
-
-            var fields = new List<ItemFields>();
-
-            view.GameSystems = items
-                .OfType<GameSystem>()
-                .OrderBy(i => i.SortName)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            var currentUserId = user.Id;
-            view.RecentlyPlayedGames = gamesWithImages
-                .OrderByDescending(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MinValue)
-                .Take(request.RecentlyPlayedGamesLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.BackdropItems = gamesWithBackdrops
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.SpotlightItems = gamesWithBackdrops
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.MultiPlayerItems = gamesWithImages
-            .Where(i => i.PlayersSupported.HasValue && i.PlayersSupported.Value > 1)
-            .Randomize()
-            .Select(i => GetItemStub(i, ImageType.Primary))
-            .Where(i => i != null)
-            .Take(1)
-            .ToList();
-
-            return ToOptimizedSerializedResultUsingCache(view);
-        }
-
-        public object Get(GetTvView request)
-        {
-            var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-            var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var series = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId)
-                .OfType<Series>()
-                .ToList();
-
-            var seriesWithBackdrops = series.Where(i => i.GetImages(ImageType.Backdrop).Any()).ToList();
-
-            var view = new TvView();
-
-            var fields = new List<ItemFields>();
-
-            var seriesWithBestBackdrops = FilterItemsForBackdropDisplay(seriesWithBackdrops).ToList();
-
-            view.BackdropItems = seriesWithBestBackdrops
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .AsParallel()
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.ShowsItems = series
-               .Where(i => i.GetImages(ImageType.Backdrop).Any())
-               .Randomize("all")
-               .Select(i => GetItemStub(i, ImageType.Backdrop))
-               .Where(i => i != null)
-               .Take(1)
-               .ToList();
-
-            view.RomanceItems = seriesWithBackdrops
-             .Where(i => i.Genres.Any(romanceGenres.ContainsKey))
-             .Randomize("romance")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.ComedyItems = seriesWithBackdrops
-             .Where(i => i.Genres.Any(comedyGenres.ContainsKey))
-             .Randomize("comedy")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            var spotlightSeries = seriesWithBestBackdrops
-                .Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8.5)
-                .ToList();
-
-            if (spotlightSeries.Count < 20)
-            {
-                spotlightSeries = seriesWithBestBackdrops;
-            }
-
-            spotlightSeries = spotlightSeries
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .ToList();
-
-            view.SpotlightItems = spotlightSeries
-                .AsParallel()
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            var miniSpotlightItems = seriesWithBackdrops
-                .Except(spotlightSeries.OfType<Series>())
-                .Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8)
-                .ToList();
-
-            if (miniSpotlightItems.Count < 15)
-            {
-                miniSpotlightItems = seriesWithBackdrops;
-            }
-
-            view.MiniSpotlights = miniSpotlightItems
-              .Randomize("minispotlight")
-              .Take(5)
-              .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-              .ToList();
-
-            var nextUpEpisodes = new TvShowsService(_userManager, _userDataManager, _libraryManager, _itemRepo, _dtoService)
-                .GetNextUpEpisodes(new GetNextUpEpisodes { UserId = user.Id }, series)
-                .ToList();
-
-            fields.Add(ItemFields.PrimaryImageAspectRatio);
-
-            view.NextUpEpisodes = nextUpEpisodes
-                .Take(request.NextUpEpisodeLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.SeriesIdsInProgress = nextUpEpisodes.Select(i => i.Series.Id.ToString("N")).ToList();
-
-            // Avoid implicitly captured closure
-            var currentUser1 = user;
-
-            var ownedEpisodes = series
-                .SelectMany(i => i.GetRecursiveChildren(currentUser1, j => j.LocationType != LocationType.Virtual))
-                .OfType<Episode>()
-                .ToList();
-
-            // Avoid implicitly captured closure
-            var currentUser = user;
-
-            view.LatestEpisodes = ownedEpisodes
-                .OrderByDescending(i => i.DateCreated)
-                .Where(i => !_userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).Played)
-                .Take(request.LatestEpisodeLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.ResumableEpisodes = ownedEpisodes
-                .Where(i => _userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).PlaybackPositionTicks > 0)
-                .OrderByDescending(i => _userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MinValue)
-                .Take(request.ResumableEpisodeLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            return ToOptimizedSerializedResultUsingCache(view);
-        }
-
-        public object Get(GetMovieView request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var items = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId)
-                .Where(i => i is Movie || i is Trailer || i is BoxSet)
-                .ToList();
-
-            var view = new MoviesView();
-
-            var movies = items.OfType<Movie>()
-                .ToList();
-
-            var trailers = items.OfType<Trailer>()
-               .ToList();
-
-            var hdMovies = movies.Where(i => i.IsHD).ToList();
-
-            var familyGenres = request.FamilyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            var familyMovies = movies.Where(i => i.Genres.Any(familyGenres.ContainsKey)).ToList();
-
-            view.HDMoviePercentage = 100 * hdMovies.Count;
-            view.HDMoviePercentage /= movies.Count;
-
-            view.FamilyMoviePercentage = 100 * familyMovies.Count;
-            view.FamilyMoviePercentage /= movies.Count;
-
-            var moviesWithBackdrops = movies
-               .Where(i => i.GetImages(ImageType.Backdrop).Any())
-               .ToList();
-
-            var fields = new List<ItemFields>();
-
-            var itemsWithTopBackdrops = FilterItemsForBackdropDisplay(moviesWithBackdrops).ToList();
-
-            view.BackdropItems = itemsWithTopBackdrops
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .AsParallel()
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.MovieItems = moviesWithBackdrops
-               .Randomize("all")
-               .Select(i => GetItemStub(i, ImageType.Backdrop))
-               .Where(i => i != null)
-               .Take(1)
-               .ToList();
-
-            view.TrailerItems = trailers
-             .Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath))
-             .Randomize()
-             .Select(i => GetItemStub(i, ImageType.Primary))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.BoxSetItems = items
-             .OfType<BoxSet>()
-             .Where(i => i.GetImages(ImageType.Backdrop).Any())
-             .Randomize()
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.ThreeDItems = moviesWithBackdrops
-             .Where(i => i.Is3D)
-             .Randomize("3d")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-            var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            view.RomanceItems = moviesWithBackdrops
-             .Where(i => i.Genres.Any(romanceGenres.ContainsKey))
-             .Randomize("romance")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.ComedyItems = moviesWithBackdrops
-             .Where(i => i.Genres.Any(comedyGenres.ContainsKey))
-             .Randomize("comedy")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.HDItems = hdMovies
-             .Where(i => i.GetImages(ImageType.Backdrop).Any())
-             .Randomize("hd")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            view.FamilyMovies = familyMovies
-             .Where(i => i.GetImages(ImageType.Backdrop).Any())
-             .Randomize("family")
-             .Select(i => GetItemStub(i, ImageType.Backdrop))
-             .Where(i => i != null)
-             .Take(1)
-             .ToList();
-
-            var currentUserId = user.Id;
-            var spotlightItems = itemsWithTopBackdrops
-                .Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8)
-                .Where(i => !_userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).Played)
-                .ToList();
-
-            if (spotlightItems.Count < 20)
-            {
-                spotlightItems = itemsWithTopBackdrops;
-            }
-
-            spotlightItems = spotlightItems
-                .OrderBy(i => Guid.NewGuid())
-                .Take(10)
-                .ToList();
-
-            view.SpotlightItems = spotlightItems
-                .AsParallel()
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            var miniSpotlightItems = moviesWithBackdrops
-                .Except(spotlightItems)
-                .Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 7.5)
-                .ToList();
-
-            if (miniSpotlightItems.Count < 15)
-            {
-                miniSpotlightItems = itemsWithTopBackdrops;
-            }
-
-            miniSpotlightItems = miniSpotlightItems
-              .Randomize("minispotlight")
-              .ToList();
-
-            // Avoid implicitly captured closure
-            miniSpotlightItems.InsertRange(0, moviesWithBackdrops
-                .Where(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).PlaybackPositionTicks > 0)
-                .OrderByDescending(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MaxValue)
-                .Take(3));
-
-            view.MiniSpotlights = miniSpotlightItems
-              .Take(3)
-              .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-              .ToList();
-
-            // Avoid implicitly captured closure
-            var currentUserId1 = user.Id;
-
-            view.LatestMovies = movies
-                .OrderByDescending(i => i.DateCreated)
-                .Where(i => !_userDataManager.GetUserData(currentUserId1, i.GetUserDataKey()).Played)
-                .Take(request.LatestMoviesLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            view.LatestTrailers = trailers
-                .OrderByDescending(i => i.DateCreated)
-                .Where(i => !_userDataManager.GetUserData(currentUserId1, i.GetUserDataKey()).Played)
-                .Take(request.LatestTrailersLimit)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
-                .ToList();
-
-            return ToOptimizedSerializedResultUsingCache(view);
-        }
-
-        private IEnumerable<BaseItem> FilterItemsForBackdropDisplay(IEnumerable<BaseItem> items)
-        {
-            var tuples = items
-                .Select(i => new Tuple<BaseItem, double>(i, GetResolution(i, ImageType.Backdrop, 0)))
-                .Where(i => i.Item2 > 0)
-                .ToList();
-
-            var topItems = tuples
-                .Where(i => i.Item2 >= 1920)
-                .ToList();
-
-            if (topItems.Count >= 10)
-            {
-                return topItems.Select(i => i.Item1);
-            }
-
-            return tuples.Select(i => i.Item1);
-        }
-
-        private double GetResolution(BaseItem item, ImageType type, int index)
-        {
-            try
-            {
-                var info = item.GetImageInfo(type, index);
-
-                var size = _imageProcessor.GetImageSize(info.Path, info.DateModified);
-
-                return size.Width;
-            }
-            catch
-            {
-                return 0;
-            }
-        }
-
-        private ItemStub GetItemStub(BaseItem item, ImageType imageType)
-        {
-            var stub = new ItemStub
-            {
-                Id = _dtoService.GetDtoId(item),
-                Name = item.Name,
-                ImageType = imageType
-            };
-
-            try
-            {
-                var tag = _imageProcessor.GetImageCacheTag(item, imageType);
-
-                if (tag != null)
-                {
-                    stub.ImageTag = tag;
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error getting image tag for {0}", ex, item.Path);
-                return null;
-            }
-
-            return stub;
-        }
-    }
-
-    static class RandomExtension
-    {
-        public static IEnumerable<T> Randomize<T>(this IEnumerable<T> sequence, string type = "none")
-            where T : BaseItem
-        {
-            var hour = DateTime.Now.Hour + DateTime.Now.Day + 2;
-
-            var typeCode = type.GetHashCode();
-
-            return sequence.OrderBy(i =>
-            {
-                var val = i.Id.GetHashCode() + i.Genres.Count + i.People.Count + (i.ProductionYear ?? 0) + i.DateCreated.Minute + i.DateModified.Minute + typeCode;
-
-                return val % hour;
-            });
-        }
-
-        public static IEnumerable<string> Randomize(this IEnumerable<string> sequence)
-        {
-            var hour = DateTime.Now.Hour + 2;
-
-            return sequence.OrderBy(i => i.GetHashCode() % hour);
-        }
-    }
-}

+ 0 - 83
MediaBrowser.Api/DefaultTheme/Models.cs

@@ -1,83 +0,0 @@
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Api.DefaultTheme
-{
-    public class ItemStub
-    {
-        public string Name { get; set; }
-        public string Id { get; set; }
-        public string ImageTag { get; set; }
-        public ImageType ImageType { get; set; }
-    }
-
-    public class MoviesView : BaseView
-    {
-        public List<ItemStub> MovieItems { get; set; }
-
-        public List<ItemStub> BoxSetItems { get; set; }
-        public List<ItemStub> TrailerItems { get; set; }
-        public List<ItemStub> HDItems { get; set; }
-        public List<ItemStub> ThreeDItems { get; set; }
-
-        public List<ItemStub> FamilyMovies { get; set; }
-
-        public List<ItemStub> RomanceItems { get; set; }
-        public List<ItemStub> ComedyItems { get; set; }
-
-        public double FamilyMoviePercentage { get; set; }
-
-        public double HDMoviePercentage { get; set; }
-
-        public List<BaseItemDto> LatestTrailers { get; set; }
-        public List<BaseItemDto> LatestMovies { get; set; }
-    }
-
-    public class TvView : BaseView
-    {
-        public List<ItemStub> ShowsItems { get; set; }
-
-        public List<ItemStub> RomanceItems { get; set; }
-        public List<ItemStub> ComedyItems { get; set; }
-
-        public List<string> SeriesIdsInProgress { get; set; }
-
-        public List<BaseItemDto> LatestEpisodes { get; set; }
-        public List<BaseItemDto> NextUpEpisodes { get; set; }
-        public List<BaseItemDto> ResumableEpisodes { get; set; }
-    }
-
-    public class ItemByNameInfo
-    {
-        public string Name { get; set; }
-        public int ItemCount { get; set; }
-    }
-
-    public class GamesView : BaseView
-    {
-        public List<ItemStub> MultiPlayerItems { get; set; }
-        public List<BaseItemDto> GameSystems { get; set; }
-        public List<BaseItemDto> RecentlyPlayedGames { get; set; }
-    }
-
-    public class BaseView
-    {
-        public List<BaseItemDto> BackdropItems { get; set; }
-        public List<BaseItemDto> SpotlightItems { get; set; }
-        public List<BaseItemDto> MiniSpotlights { get; set; }
-    }
-
-    public class FavoritesView : BaseView
-    {
-        public List<BaseItemDto> Movies { get; set; }
-        public List<BaseItemDto> Series { get; set; }
-        public List<BaseItemDto> Episodes { get; set; }
-        public List<BaseItemDto> Games { get; set; }
-        public List<BaseItemDto> Books { get; set; }
-        public List<BaseItemDto> Albums { get; set; }
-        public List<BaseItemDto> Songs { get; set; }
-        public List<BaseItemDto> Artists { get; set; }
-    }
-}

+ 3 - 1
MediaBrowser.Api/DisplayPreferencesService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Serialization;
 using ServiceStack;
@@ -48,6 +49,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class DisplayPreferencesService
     /// </summary>
+    [Authenticated]
     public class DisplayPreferencesService : BaseApiService
     {
         /// <summary>

+ 11 - 6
MediaBrowser.Api/Dlna/DlnaServerService.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Configuration;
 using ServiceStack;
 using ServiceStack.Text.Controller;
 using ServiceStack.Web;
@@ -76,11 +78,14 @@ namespace MediaBrowser.Api.Dlna
         private readonly IContentDirectory _contentDirectory;
         private readonly IConnectionManager _connectionManager;
 
-        public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager)
+        private readonly IConfigurationManager _config;
+
+        public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IConfigurationManager config)
         {
             _dlnaManager = dlnaManager;
             _contentDirectory = contentDirectory;
             _connectionManager = connectionManager;
+            _config = config;
         }
 
         public object Get(GetDescriptionXml request)
@@ -104,16 +109,16 @@ namespace MediaBrowser.Api.Dlna
             return ResultFactory.GetResult(xml, "text/xml");
         }
 
-        public object Post(ProcessContentDirectoryControlRequest request)
+        public async Task<object> Post(ProcessContentDirectoryControlRequest request)
         {
-            var response = PostAsync(request.RequestStream, _contentDirectory).Result;
+            var response = await PostAsync(request.RequestStream, _contentDirectory).ConfigureAwait(false);
 
             return ResultFactory.GetResult(response.Xml, "text/xml");
         }
 
-        public object Post(ProcessConnectionManagerControlRequest request)
+        public async Task<object> Post(ProcessConnectionManagerControlRequest request)
         {
-            var response = PostAsync(request.RequestStream, _connectionManager).Result;
+            var response = await PostAsync(request.RequestStream, _connectionManager).ConfigureAwait(false);
 
             return ResultFactory.GetResult(response.Xml, "text/xml");
         }

+ 2 - 0
MediaBrowser.Api/Dlna/DlnaService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using ServiceStack;
 using System.Collections.Generic;
@@ -42,6 +43,7 @@ namespace MediaBrowser.Api.Dlna
     {
     }
 
+    [Authenticated]
     public class DlnaService : BaseApiService
     {
         private readonly IDlnaManager _dlnaManager;

+ 2 - 0
MediaBrowser.Api/EnvironmentService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using ServiceStack;
@@ -86,6 +87,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class EnvironmentService
     /// </summary>
+    [Authenticated]
     public class EnvironmentService : BaseApiService
     {
         const char UncSeparator = '\\';

+ 2 - 0
MediaBrowser.Api/GamesService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using ServiceStack;
@@ -51,6 +52,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class GamesService
     /// </summary>
+    [Authenticated]
     public class GamesService : BaseApiService
     {
         /// <summary>

+ 6 - 2
MediaBrowser.Api/Images/ImageByNameService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Dto;
@@ -100,13 +101,16 @@ namespace MediaBrowser.Api.Images
         /// </summary>
         private readonly IServerApplicationPaths _appPaths;
 
+        private readonly IFileSystem _fileSystem;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageByNameService" /> class.
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
-        public ImageByNameService(IServerApplicationPaths appPaths)
+        public ImageByNameService(IServerApplicationPaths appPaths, IFileSystem fileSystem)
         {
             _appPaths = appPaths;
+            _fileSystem = fileSystem;
         }
 
         public object Get(GetMediaInfoImages request)
@@ -133,7 +137,7 @@ namespace MediaBrowser.Api.Images
                     .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.Ordinal))
                     .Select(i => new ImageByNameInfo
                     {
-                        Name = Path.GetFileNameWithoutExtension(i.FullName),
+                        Name = _fileSystem.GetFileNameWithoutExtension(i),
                         FileLength = i.Length,
 
                         // For themeable images, use the Theme property

+ 84 - 58
MediaBrowser.Api/Images/ImageService.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
@@ -13,7 +14,6 @@ using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -26,6 +26,7 @@ namespace MediaBrowser.Api.Images
     /// </summary>
     [Route("/Items/{Id}/Images", "GET")]
     [Api(Description = "Gets information about an item's images")]
+    [Authenticated]
     public class GetItemImageInfos : IReturn<List<ImageInfo>>
     {
         /// <summary>
@@ -38,6 +39,8 @@ namespace MediaBrowser.Api.Images
 
     [Route("/Items/{Id}/Images/{Type}", "GET")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
+    [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "GET")]
+    [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "HEAD")]
     [Api(Description = "Gets an item image")]
     public class GetItemImage : ImageRequest
     {
@@ -47,8 +50,6 @@ namespace MediaBrowser.Api.Images
         /// <value>The id.</value>
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
-
-        public string Params { get; set; }
     }
 
     /// <summary>
@@ -56,6 +57,7 @@ namespace MediaBrowser.Api.Images
     /// </summary>
     [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST")]
     [Api(Description = "Updates the index for an item image")]
+    [Authenticated]
     public class UpdateItemImageIndex : IReturnVoid
     {
         /// <summary>
@@ -137,6 +139,7 @@ namespace MediaBrowser.Api.Images
     [Route("/Items/{Id}/Images/{Type}", "DELETE")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
     [Api(Description = "Deletes an item image")]
+    [Authenticated]
     public class DeleteItemImage : DeleteImageRequest, IReturnVoid
     {
         /// <summary>
@@ -153,6 +156,7 @@ namespace MediaBrowser.Api.Images
     [Route("/Users/{Id}/Images/{Type}", "DELETE")]
     [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")]
     [Api(Description = "Deletes a user image")]
+    [Authenticated]
     public class DeleteUserImage : DeleteImageRequest, IReturnVoid
     {
         /// <summary>
@@ -169,6 +173,7 @@ namespace MediaBrowser.Api.Images
     [Route("/Users/{Id}/Images/{Type}", "POST")]
     [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")]
     [Api(Description = "Posts a user image")]
+    [Authenticated]
     public class PostUserImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
     {
         /// <summary>
@@ -191,6 +196,7 @@ namespace MediaBrowser.Api.Images
     [Route("/Items/{Id}/Images/{Type}", "POST")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
     [Api(Description = "Posts an item image")]
+    [Authenticated]
     public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
     {
         /// <summary>
@@ -355,49 +361,25 @@ namespace MediaBrowser.Api.Images
         /// <returns>System.Object.</returns>
         public object Get(GetItemImage request)
         {
-            var item = string.IsNullOrEmpty(request.Id) ? 
+            var item = string.IsNullOrEmpty(request.Id) ?
                 _libraryManager.RootFolder :
                 _libraryManager.GetItemById(request.Id);
 
-            if (!string.IsNullOrEmpty(request.Params))
-            {
-                ParseOptions(request, request.Params);
-            }
-
-            return GetImage(request, item);
+            return GetImage(request, item, false);
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-        private void ParseOptions(ImageRequest request, string options)
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Head(GetItemImage request)
         {
-            var vals = options.Split(';');
-
-            for (var i = 0; i < vals.Length; i++)
-            {
-                var val = vals[i];
-
-                if (string.IsNullOrWhiteSpace(val))
-                {
-                    continue;
-                }
+            var item = string.IsNullOrEmpty(request.Id) ?
+                _libraryManager.RootFolder :
+                _libraryManager.GetItemById(request.Id);
 
-                if (i == 0)
-                {
-                    request.Tag = val;
-                }
-                else if (i == 1)
-                {
-                    request.Format = (ImageOutputFormat)Enum.Parse(typeof(ImageOutputFormat), val, true);
-                }
-                else if (i == 2)
-                {
-                    request.MaxWidth = int.Parse(val, _usCulture);
-                }
-                else if (i == 3)
-                {
-                    request.MaxHeight = int.Parse(val, _usCulture);
-                }
-            }
+            return GetImage(request, item, true);
         }
 
         /// <summary>
@@ -409,7 +391,7 @@ namespace MediaBrowser.Api.Images
         {
             var item = _userManager.Users.First(i => i.Id == request.Id);
 
-            return GetImage(request, item);
+            return GetImage(request, item, false);
         }
 
         public object Get(GetItemByNameImage request)
@@ -419,7 +401,7 @@ namespace MediaBrowser.Api.Images
 
             var item = GetItemByName(request.Name, type, _libraryManager);
 
-            return GetImage(request, item);
+            return GetImage(request, item, false);
         }
 
         /// <summary>
@@ -516,10 +498,10 @@ namespace MediaBrowser.Api.Images
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="item">The item.</param>
+        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
         /// <returns>System.Object.</returns>
-        /// <exception cref="ResourceNotFoundException">
-        /// </exception>
-        public object GetImage(ImageRequest request, IHasImages item)
+        /// <exception cref="ResourceNotFoundException"></exception>
+        public object GetImage(ImageRequest request, IHasImages item, bool isHeadRequest)
         {
             var imageInfo = GetImageInfo(request, item);
 
@@ -528,9 +510,6 @@ namespace MediaBrowser.Api.Images
                 throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
             }
 
-            // See if we can avoid a file system lookup by looking for the file in ResolveArgs
-            var originalFileImageDateModified = imageInfo.DateModified;
-
             var supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.ImageEnhancers.Where(i =>
             {
                 try
@@ -557,25 +536,68 @@ namespace MediaBrowser.Api.Images
                 cacheDuration = TimeSpan.FromDays(365);
             }
 
-            // Avoid implicitly captured closure
-            var currentItem = item;
-            var currentRequest = request;
-
             var responseHeaders = new Dictionary<string, string>
             {
                 {"transferMode.dlna.org", "Interactive"},
                 {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
             };
 
-            return ToCachedResult(cacheGuid, originalFileImageDateModified, cacheDuration, () => new ImageWriter
+            return GetImageResult(item,
+                request,
+                imageInfo,
+                supportedImageEnhancers,
+                contentType,
+                cacheDuration,
+                responseHeaders,
+                isHeadRequest)
+                .Result;
+        }
+
+        private async Task<object> GetImageResult(IHasImages item,
+            ImageRequest request,
+            ItemImageInfo image,
+            List<IImageEnhancer> enhancers,
+            string contentType,
+            TimeSpan? cacheDuration,
+            IDictionary<string, string> headers,
+            bool isHeadRequest)
+        {
+            var cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
+
+            if (request.CropWhitespace.HasValue)
             {
-                Item = currentItem,
-                Request = currentRequest,
-                Enhancers = supportedImageEnhancers,
-                Image = imageInfo,
-                ImageProcessor = _imageProcessor
+                cropwhitespace = request.CropWhitespace.Value;
+            }
+
+            var options = new ImageProcessingOptions
+            {
+                CropWhiteSpace = cropwhitespace,
+                Enhancers = enhancers,
+                Height = request.Height,
+                ImageIndex = request.Index ?? 0,
+                Image = image,
+                Item = item,
+                MaxHeight = request.MaxHeight,
+                MaxWidth = request.MaxWidth,
+                Quality = request.Quality,
+                Width = request.Width,
+                OutputFormat = request.Format,
+                AddPlayedIndicator = request.AddPlayedIndicator,
+                PercentPlayed = request.PercentPlayed,
+                UnplayedCount = request.UnplayedCount,
+                BackgroundColor = request.BackgroundColor
+            };
 
-            }, contentType, responseHeaders);
+            var file = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
+
+            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                CacheDuration = cacheDuration,
+                ResponseHeaders = headers,
+                ContentType = contentType,
+                IsHeadRequest = isHeadRequest,
+                Path = file
+            });
         }
 
         private string GetMimeType(ImageOutputFormat format, string path)
@@ -596,6 +618,10 @@ namespace MediaBrowser.Api.Images
             {
                 return Common.Net.MimeTypes.GetMimeType("i.png");
             }
+            if (format == ImageOutputFormat.Webp)
+            {
+                return Common.Net.MimeTypes.GetMimeType("i.webp");
+            }
 
             return Common.Net.MimeTypes.GetMimeType(path);
         }

+ 0 - 96
MediaBrowser.Api/Images/ImageWriter.cs

@@ -1,96 +0,0 @@
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using ServiceStack.Web;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.Images
-{
-    /// <summary>
-    /// Class ImageWriter
-    /// </summary>
-    public class ImageWriter : IStreamWriter, IHasOptions
-    {
-        public List<IImageEnhancer> Enhancers;
-
-        /// <summary>
-        /// Gets or sets the request.
-        /// </summary>
-        /// <value>The request.</value>
-        public ImageRequest Request { get; set; }
-        /// <summary>
-        /// Gets or sets the item.
-        /// </summary>
-        /// <value>The item.</value>
-        public IHasImages Item { get; set; }
-        /// <summary>
-        /// The original image date modified
-        /// </summary>
-        public ItemImageInfo Image;
-
-        public IImageProcessor ImageProcessor { get; set; }
-
-        /// <summary>
-        /// The _options
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Options
-        {
-            get { return _options; }
-        }
-
-        /// <summary>
-        /// Writes to.
-        /// </summary>
-        /// <param name="responseStream">The response stream.</param>
-        public void WriteTo(Stream responseStream)
-        {
-            var task = WriteToAsync(responseStream);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Writes to async.
-        /// </summary>
-        /// <param name="responseStream">The response stream.</param>
-        /// <returns>Task.</returns>
-        private Task WriteToAsync(Stream responseStream)
-        {
-            var cropwhitespace = Request.Type == ImageType.Logo || Request.Type == ImageType.Art;
-
-            if (Request.CropWhitespace.HasValue)
-            {
-                cropwhitespace = Request.CropWhitespace.Value;
-            }
-
-            var options = new ImageProcessingOptions
-            {
-                CropWhiteSpace = cropwhitespace,
-                Enhancers = Enhancers,
-                Height = Request.Height,
-                ImageIndex = Request.Index ?? 0,
-                Image = Image,
-                Item = Item,
-                MaxHeight = Request.MaxHeight,
-                MaxWidth = Request.MaxWidth,
-                Quality = Request.Quality,
-                Width = Request.Width,
-                OutputFormat = Request.Format,
-                AddPlayedIndicator = Request.AddPlayedIndicator,
-                PercentPlayed = Request.PercentPlayed,
-                UnplayedCount = Request.UnplayedCount,
-                BackgroundColor = Request.BackgroundColor
-            };
-
-            return ImageProcessor.ProcessImage(options, responseStream);
-        }
-    }
-}

+ 10 - 19
MediaBrowser.Api/Images/RemoteImageService.cs

@@ -199,35 +199,33 @@ namespace MediaBrowser.Api.Images
             return _providerManager.GetRemoteImageProviderInfo(item).ToList();
         }
 
-        public object Get(GetRemoteImages request)
+        public async Task<object> Get(GetRemoteImages request)
         {
             var item = _libraryManager.GetItemById(request.Id);
 
-            var result = GetRemoteImageResult(item, request);
-
-            return ToOptimizedSerializedResultUsingCache(result);
+            return await GetRemoteImageResult(item, request).ConfigureAwait(false);
         }
 
-        public object Get(GetItemByNameRemoteImages request)
+        public async Task<object> Get(GetItemByNameRemoteImages request)
         {
             var pathInfo = PathInfo.Parse(Request.PathInfo);
             var type = pathInfo.GetArgumentValue<string>(0);
 
             var item = GetItemByName(request.Name, type, _libraryManager);
 
-            return GetRemoteImageResult(item, request);
+            return await GetRemoteImageResult(item, request).ConfigureAwait(false);
         }
 
-        private RemoteImageResult GetRemoteImageResult(BaseItem item, BaseRemoteImageRequest request)
+        private async Task<RemoteImageResult> GetRemoteImageResult(BaseItem item, BaseRemoteImageRequest request)
         {
-            var images = _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery
+            var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery
             {
                 ProviderName = request.ProviderName,
                 IncludeAllLanguages = request.IncludeAllLanguages,
                 IncludeDisabledProviders = true,
                 ImageType = request.Type
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             var imagesList = images.ToList();
 
@@ -308,17 +306,10 @@ namespace MediaBrowser.Api.Images
         /// <returns>System.Object.</returns>
         public object Get(GetRemoteImage request)
         {
-            var task = GetRemoteImage(request);
-
-            return task.Result;
+            return GetAsync(request).Result;
         }
 
-        /// <summary>
-        /// Gets the remote image.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetRemoteImage(GetRemoteImage request)
+        public async Task<object> GetAsync(GetRemoteImage request)
         {
             var urlHash = request.ImageUrl.GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
@@ -356,7 +347,7 @@ namespace MediaBrowser.Api.Images
 
             return ToStaticFileResult(contentPath);
         }
-
+        
         /// <summary>
         /// Downloads the image.
         /// </summary>

+ 25 - 6
MediaBrowser.Api/ItemLookupService.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
@@ -21,6 +22,7 @@ namespace MediaBrowser.Api
 {
     [Route("/Items/{Id}/ExternalIdInfos", "GET")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetExternalIdInfos : IReturn<List<ExternalIdInfo>>
     {
         /// <summary>
@@ -33,54 +35,63 @@ namespace MediaBrowser.Api
 
     [Route("/Items/RemoteSearch/Movie", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/Trailer", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/AdultVideo", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetAdultVideoRemoteSearchResults : RemoteSearchQuery<ItemLookupInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/Series", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetSeriesRemoteSearchResults : RemoteSearchQuery<SeriesInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/Game", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetGameRemoteSearchResults : RemoteSearchQuery<GameInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/BoxSet", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetBoxSetRemoteSearchResults : RemoteSearchQuery<BoxSetInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/MusicArtist", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery<ArtistInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/MusicAlbum", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery<AlbumInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
 
     [Route("/Items/RemoteSearch/Person", "POST")]
     [Api(Description = "Gets external id infos for an item")]
+    [Authenticated]
     public class GetPersonRemoteSearchResults : RemoteSearchQuery<PersonLookupInfo>, IReturn<List<RemoteSearchResult>>
     {
     }
@@ -98,6 +109,7 @@ namespace MediaBrowser.Api
 
     [Route("/Items/RemoteSearch/Apply/{Id}", "POST")]
     [Api(Description = "Applies search criteria to an item and refreshes metadata")]
+    [Authenticated]
     public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
@@ -212,16 +224,23 @@ namespace MediaBrowser.Api
                 }
             }
 
-            var task = item.RefreshMetadata(new MetadataRefreshOptions
+            var service = new ItemRefreshService(_libraryManager)
             {
+                Logger = Logger,
+                Request = Request,
+                ResultFactory = ResultFactory,
+                SessionContext = SessionContext
+            };
+
+            service.Post(new RefreshItem
+            {
+                Id = request.Id,
                 MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
                 ImageRefreshMode = ImageRefreshMode.FullRefresh,
                 ReplaceAllMetadata = true,
-                ReplaceAllImages = true
-
-            }, CancellationToken.None);
-
-            Task.WaitAll(task);
+                ReplaceAllImages = true,
+                Recursive = true
+            });
         }
 
         /// <summary>

+ 17 - 8
MediaBrowser.Api/ItemRefreshService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using ServiceStack;
 using System;
@@ -12,10 +13,16 @@ namespace MediaBrowser.Api
 {
     public class BaseRefreshRequest : IReturnVoid
     {
-        [ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool Forced { get; set; }
+        [ApiMember(Name = "MetadataRefreshMode", Description = "Specifies the metadata refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public MetadataRefreshMode MetadataRefreshMode { get; set; }
 
-        [ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced during the refresh.", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        [ApiMember(Name = "ImageRefreshMode", Description = "Specifies the image refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public ImageRefreshMode ImageRefreshMode { get; set; }
+
+        [ApiMember(Name = "ReplaceAllMetadata", Description = "Determines if metadata should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public bool ReplaceAllMetadata { get; set; }
+
+        [ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
         public bool ReplaceAllImages { get; set; }
     }
 
@@ -30,6 +37,7 @@ namespace MediaBrowser.Api
         public string Id { get; set; }
     }
 
+    [Authenticated]
     public class ItemRefreshService : BaseApiService
     {
         private readonly ILibraryManager _libraryManager;
@@ -91,7 +99,7 @@ namespace MediaBrowser.Api
         private async Task RefreshItem(RefreshItem request, BaseItem item)
         {
             var options = GetRefreshOptions(request);
-            
+
             try
             {
                 await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
@@ -146,10 +154,11 @@ namespace MediaBrowser.Api
         {
             return new MetadataRefreshOptions
             {
-                MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                ImageRefreshMode = ImageRefreshMode.FullRefresh,
-                ReplaceAllMetadata = request.Forced,
-                ReplaceAllImages = request.ReplaceAllImages
+                MetadataRefreshMode = request.MetadataRefreshMode,
+                ImageRefreshMode = request.ImageRefreshMode,
+                ReplaceAllImages = request.ReplaceAllImages,
+                ReplaceAllMetadata = request.ReplaceAllMetadata,
+                ForceSave = true
             };
         }
     }

+ 18 - 5
MediaBrowser.Api/ItemUpdateService.cs

@@ -1,11 +1,12 @@
-using System.Collections.Generic;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using ServiceStack;
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -19,6 +20,7 @@ namespace MediaBrowser.Api
         public string ItemId { get; set; }
     }
 
+    [Authenticated]
     public class ItemUpdateService : BaseApiService
     {
         private readonly ILibraryManager _libraryManager;
@@ -63,6 +65,11 @@ namespace MediaBrowser.Api
             }
         }
 
+        private DateTime NormalizeDateTime(DateTime val)
+        {
+            return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+        }
+
         private void UpdateItem(BaseItemDto request, BaseItem item)
         {
             item.Name = request.Name;
@@ -108,6 +115,12 @@ namespace MediaBrowser.Api
                 hasTags.Tags = request.Tags;
             }
 
+            var hasTaglines = item as IHasTaglines;
+            if (hasTaglines != null)
+            {
+                hasTaglines.Taglines = request.Taglines;
+            }
+
             var hasShortOverview = item as IHasShortOverview;
             if (hasShortOverview != null)
             {
@@ -132,11 +145,11 @@ namespace MediaBrowser.Api
 
             if (request.DateCreated.HasValue)
             {
-                item.DateCreated = request.DateCreated.Value.ToUniversalTime();
+                item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
             }
 
-            item.EndDate = request.EndDate.HasValue ? request.EndDate.Value.ToUniversalTime() : (DateTime?)null;
-            item.PremiereDate = request.PremiereDate.HasValue ? request.PremiereDate.Value.ToUniversalTime() : (DateTime?)null;
+            item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
+            item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
             item.ProductionYear = request.ProductionYear;
             item.OfficialRating = request.OfficialRating;
             item.CustomRating = request.CustomRating;

+ 2 - 0
MediaBrowser.Api/Library/ChapterService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Net;
 using ServiceStack;
 using System.Linq;
 
@@ -9,6 +10,7 @@ namespace MediaBrowser.Api.Library
     {
     }
 
+    [Authenticated]
     public class ChapterService : BaseApiService
     {
         private readonly IChapterManager _chapterManager;

+ 2 - 0
MediaBrowser.Api/Library/FileOrganizationService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.FileOrganization;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
@@ -78,6 +79,7 @@ namespace MediaBrowser.Api.Library
         public bool RememberCorrection { get; set; }
     }
 
+    [Authenticated]
     public class FileOrganizationService : BaseApiService
     {
         private readonly IFileOrganizationService _iFileOrganizationService;

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

@@ -65,7 +65,7 @@ namespace MediaBrowser.Api.Library
             var rootFolderPath = appPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
-            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
+            var shortcutFilename = fileSystem.GetFileNameWithoutExtension(path);
 
             var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 

+ 20 - 16
MediaBrowser.Api/Library/LibraryService.cs

@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
@@ -210,20 +211,23 @@ namespace MediaBrowser.Api.Library
     [Api(Description = "Gets all user media folders.")]
     public class GetMediaFolders : IReturn<ItemsResult>
     {
-
+        [ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? IsHidden { get; set; }
     }
 
+    [Route("/Library/Series/Added", "POST")]
     [Route("/Library/Series/Updated", "POST")]
     [Api(Description = "Reports that new episodes of a series have been added by an external source")]
     public class PostUpdatedSeries : IReturnVoid
     {
-        [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string TvdbId { get; set; }
     }
 
     /// <summary>
     /// Class LibraryService
     /// </summary>
+    [Authenticated]
     public class LibraryService : BaseApiService
     {
         /// <summary>
@@ -258,6 +262,13 @@ namespace MediaBrowser.Api.Library
         {
             var items = _libraryManager.GetUserRootFolder().Children.OrderBy(i => i.SortName).ToList();
 
+            if (request.IsHidden.HasValue)
+            {
+                var val = request.IsHidden.Value;
+
+                items = items.Where(i => i.IsHidden == val).ToList();
+            }
+
             // Get everything
             var fields = Enum.GetNames(typeof(ItemFields))
                     .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
@@ -273,6 +284,11 @@ namespace MediaBrowser.Api.Library
             return ToOptimizedResult(result);
         }
 
+        public void Post(PostUpdatedSeries request)
+        {
+            Task.Run(() => _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None));
+        }
+
         public object Get(GetFile request)
         {
             var item = _libraryManager.GetItemById(request.Id);
@@ -451,24 +467,12 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         /// <param name="request">The request.</param>
         public void Delete(DeleteItem request)
-        {
-            var task = DeleteItem(request);
-
-            Task.WaitAll(task);
-        }
-
-        private Task DeleteItem(DeleteItem request)
         {
             var item = _libraryManager.GetItemById(request.Id);
 
-            var session = GetSession(_sessionManager);
+            var task = _libraryManager.DeleteItem(item);
 
-            if (!session.UserId.HasValue || !_userManager.GetUserById(session.UserId.Value).Configuration.EnableContentDeletion)
-            {
-                throw new UnauthorizedAccessException("This operation requires a logged in user with delete access.");
-            }
-
-            return _libraryManager.DeleteItem(item);
+            Task.WaitAll(task);
         }
 
         /// <summary>

+ 2 - 26
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using ServiceStack;
@@ -130,36 +131,11 @@ namespace MediaBrowser.Api.Library
         /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
         public bool RefreshLibrary { get; set; }
     }
-
-    [Route("/Library/Downloaded", "POST")]
-    public class ReportContentDownloaded : IReturnVoid
-    {
-        [ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Path { get; set; }
-
-        [ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ImageUrl { get; set; }
-
-        [ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-    }
-
-    [Route("/Library/Downloading", "POST")]
-    public class ReportContentDownloading : IReturnVoid
-    {
-        [ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Path { get; set; }
-
-        [ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ImageUrl { get; set; }
-
-        [ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-    }
     
     /// <summary>
     /// Class LibraryStructureService
     /// </summary>
+    [Authenticated]
     public class LibraryStructureService : BaseApiService
     {
         /// <summary>

+ 42 - 40
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
@@ -267,6 +268,7 @@ namespace MediaBrowser.Api.LiveTv
         public string UserId { get; set; }
     }
 
+    [Authenticated]
     public class LiveTvService : BaseApiService
     {
         private readonly ILiveTvManager _liveTvManager;
@@ -280,7 +282,7 @@ namespace MediaBrowser.Api.LiveTv
 
         private void AssertUserCanManageLiveTv()
         {
-            var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, _userManager);
+            var user = SessionContext.GetUser(Request);
 
             if (user == null)
             {
@@ -293,16 +295,16 @@ namespace MediaBrowser.Api.LiveTv
             }
         }
 
-        public object Get(GetLiveTvInfo request)
+        public async Task<object> Get(GetLiveTvInfo request)
         {
-            var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None).Result;
+            var info = await _liveTvManager.GetLiveTvInfo(CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(info);
         }
 
-        public object Get(GetChannels request)
+        public async Task<object> Get(GetChannels request)
         {
-            var result = _liveTvManager.GetChannels(new LiveTvChannelQuery
+            var result = await _liveTvManager.GetChannels(new LiveTvChannelQuery
             {
                 ChannelType = request.Type,
                 UserId = request.UserId,
@@ -312,26 +314,26 @@ namespace MediaBrowser.Api.LiveTv
                 IsLiked = request.IsLiked,
                 IsDisliked = request.IsDisliked
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetChannel request)
+        public async Task<object> Get(GetChannel request)
         {
             var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
 
-            var result = _liveTvManager.GetChannel(request.Id, CancellationToken.None, user).Result;
+            var result = await _liveTvManager.GetChannel(request.Id, CancellationToken.None, user).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetLiveTvFolder request)
+        public async Task<object> Get(GetLiveTvFolder request)
         {
-            return ToOptimizedResult(_liveTvManager.GetLiveTvFolder(request.UserId, CancellationToken.None).Result);
+            return ToOptimizedResult(await _liveTvManager.GetLiveTvFolder(request.UserId, CancellationToken.None).ConfigureAwait(false));
         }
 
-        public object Get(GetPrograms request)
+        public async Task<object> Get(GetPrograms request)
         {
             var query = new ProgramQuery
             {
@@ -359,12 +361,12 @@ namespace MediaBrowser.Api.LiveTv
                 query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
             }
 
-            var result = _liveTvManager.GetPrograms(query, CancellationToken.None).Result;
+            var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetRecommendedPrograms request)
+        public async Task<object> Get(GetRecommendedPrograms request)
         {
             var query = new RecommendedProgramQuery
             {
@@ -374,7 +376,7 @@ namespace MediaBrowser.Api.LiveTv
                 HasAired = request.HasAired
             };
 
-            var result = _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).Result;
+            var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -384,9 +386,9 @@ namespace MediaBrowser.Api.LiveTv
             return Get(request);
         }
 
-        public object Get(GetRecordings request)
+        public async Task<object> Get(GetRecordings request)
         {
-            var result = _liveTvManager.GetRecordings(new RecordingQuery
+            var result = await _liveTvManager.GetRecordings(new RecordingQuery
             {
                 ChannelId = request.ChannelId,
                 UserId = request.UserId,
@@ -397,35 +399,35 @@ namespace MediaBrowser.Api.LiveTv
                 SeriesTimerId = request.SeriesTimerId,
                 IsInProgress = request.IsInProgress
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetRecording request)
+        public async Task<object> Get(GetRecording request)
         {
             var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
 
-            var result = _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).Result;
+            var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetTimer request)
+        public async Task<object> Get(GetTimer request)
         {
-            var result = _liveTvManager.GetTimer(request.Id, CancellationToken.None).Result;
+            var result = await _liveTvManager.GetTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetTimers request)
+        public async Task<object> Get(GetTimers request)
         {
-            var result = _liveTvManager.GetTimers(new TimerQuery
+            var result = await _liveTvManager.GetTimers(new TimerQuery
             {
                 ChannelId = request.ChannelId,
                 SeriesTimerId = request.SeriesTimerId
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -457,21 +459,21 @@ namespace MediaBrowser.Api.LiveTv
             Task.WaitAll(task);
         }
 
-        public object Get(GetSeriesTimers request)
+        public async Task<object> Get(GetSeriesTimers request)
         {
-            var result = _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
+            var result = await _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
             {
                 SortOrder = request.SortOrder,
                 SortBy = request.SortBy
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetSeriesTimer request)
+        public async Task<object> Get(GetSeriesTimer request)
         {
-            var result = _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).Result;
+            var result = await _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -494,27 +496,27 @@ namespace MediaBrowser.Api.LiveTv
             Task.WaitAll(task);
         }
 
-        public object Get(GetDefaultTimer request)
+        public async Task<object> Get(GetDefaultTimer request)
         {
             if (string.IsNullOrEmpty(request.ProgramId))
             {
-                var result = _liveTvManager.GetNewTimerDefaults(CancellationToken.None).Result;
+                var result = await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false);
 
                 return ToOptimizedSerializedResultUsingCache(result);
             }
             else
             {
-                var result = _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).Result;
+                var result = await _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).ConfigureAwait(false);
 
                 return ToOptimizedSerializedResultUsingCache(result);
             }
         }
 
-        public object Get(GetProgram request)
+        public async Task<object> Get(GetProgram request)
         {
             var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
 
-            var result = _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).Result;
+            var result = await _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -537,23 +539,23 @@ namespace MediaBrowser.Api.LiveTv
             Task.WaitAll(task);
         }
 
-        public object Get(GetRecordingGroups request)
+        public async Task<object> Get(GetRecordingGroups request)
         {
-            var result = _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
+            var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
             {
                 UserId = request.UserId
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetRecordingGroup request)
+        public async Task<object> Get(GetRecordingGroup request)
         {
-            var result = _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
+            var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
             {
 
-            }, CancellationToken.None).Result;
+            }, CancellationToken.None).ConfigureAwait(false);
 
             var group = result.Items.FirstOrDefault(i => string.Equals(i.Id, request.Id, StringComparison.OrdinalIgnoreCase));
 

+ 2 - 0
MediaBrowser.Api/LocalizationService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using ServiceStack;
@@ -42,6 +43,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class CulturesService
     /// </summary>
+    [Authenticated]
     public class LocalizationService : BaseApiService
     {
         /// <summary>

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

@@ -49,6 +49,9 @@
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="MoreLinq">
+      <HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="Microsoft.CSharp" />
@@ -65,27 +68,25 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="BrandingService.cs" />
     <Compile Include="ChannelService.cs" />
     <Compile Include="Dlna\DlnaServerService.cs" />
     <Compile Include="Dlna\DlnaService.cs" />
     <Compile Include="Library\ChapterService.cs" />
-    <Compile Include="Library\SubtitleService.cs" />
+    <Compile Include="PlaylistService.cs" />
+    <Compile Include="Subtitles\SubtitleService.cs" />
     <Compile Include="Movies\CollectionService.cs" />
     <Compile Include="Music\AlbumsService.cs" />
     <Compile Include="AppThemeService.cs" />
     <Compile Include="BaseApiService.cs" />
     <Compile Include="ConfigurationService.cs" />
-    <Compile Include="DefaultTheme\DefaultThemeService.cs" />
-    <Compile Include="DefaultTheme\Models.cs" />
     <Compile Include="DisplayPreferencesService.cs" />
     <Compile Include="EnvironmentService.cs" />
-    <Compile Include="AuthorizationRequestFilterAttribute.cs" />
     <Compile Include="GamesService.cs" />
     <Compile Include="IHasItemFields.cs" />
     <Compile Include="Images\ImageByNameService.cs" />
     <Compile Include="Images\ImageRequest.cs" />
     <Compile Include="Images\ImageService.cs" />
-    <Compile Include="Images\ImageWriter.cs" />
     <Compile Include="Music\InstantMixService.cs" />
     <Compile Include="ItemLookupService.cs" />
     <Compile Include="ItemRefreshService.cs" />
@@ -101,7 +102,7 @@
     <Compile Include="NotificationsService.cs" />
     <Compile Include="PackageReviewService.cs" />
     <Compile Include="PackageService.cs" />
-    <Compile Include="Playback\EndlessStreamCopy.cs" />
+    <Compile Include="Playback\BifService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
     <Compile Include="Playback\Hls\DynamicHlsService.cs" />
     <Compile Include="Playback\Hls\HlsSegmentService.cs" />
@@ -120,9 +121,12 @@
     <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
     <Compile Include="ApiEntryPoint.cs" />
     <Compile Include="SearchService.cs" />
-    <Compile Include="SessionsService.cs" />
+    <Compile Include="Session\SessionsService.cs" />
     <Compile Include="SimilarItemsHelper.cs" />
-    <Compile Include="SystemService.cs" />
+    <Compile Include="Sync\SyncService.cs" />
+    <Compile Include="System\ActivityLogService.cs" />
+    <Compile Include="System\ActivityLogWebSocketListener.cs" />
+    <Compile Include="System\SystemService.cs" />
     <Compile Include="Movies\TrailersService.cs" />
     <Compile Include="TvShowsService.cs" />
     <Compile Include="UserLibrary\ArtistsService.cs" />
@@ -133,15 +137,15 @@
     <Compile Include="UserLibrary\ItemsService.cs" />
     <Compile Include="UserLibrary\MusicGenresService.cs" />
     <Compile Include="UserLibrary\PersonsService.cs" />
+    <Compile Include="UserLibrary\PlaystateService.cs" />
     <Compile Include="UserLibrary\StudiosService.cs" />
     <Compile Include="UserLibrary\UserLibraryService.cs" />
     <Compile Include="UserLibrary\YearsService.cs" />
     <Compile Include="UserService.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="VideosService.cs" />
-    <Compile Include="WebSocket\LogFileWebSocketListener.cs" />
-    <Compile Include="WebSocket\SessionInfoWebSocketListener.cs" />
-    <Compile Include="WebSocket\SystemInfoWebSocketListener.cs" />
+    <Compile Include="Session\SessionInfoWebSocketListener.cs" />
+    <Compile Include="System\SystemInfoWebSocketListener.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
@@ -160,13 +164,12 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
-  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <PropertyGroup>
     <PostBuildEvent>
     </PostBuildEvent>
   </PropertyGroup>
-  <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
+  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">

+ 6 - 9
MediaBrowser.Api/Movies/CollectionService.cs

@@ -1,5 +1,7 @@
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Collections;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System;
@@ -45,6 +47,7 @@ namespace MediaBrowser.Api.Movies
         public Guid Id { get; set; }
     }
 
+    [Authenticated]
     public class CollectionService : BaseApiService
     {
         private readonly ICollectionManager _collectionManager;
@@ -56,17 +59,16 @@ namespace MediaBrowser.Api.Movies
             _dtoService = dtoService;
         }
 
-        public object Post(CreateCollection request)
+        public async Task<object> Post(CreateCollection request)
         {
-            var task = _collectionManager.CreateCollection(new CollectionCreationOptions
+            var item = await _collectionManager.CreateCollection(new CollectionCreationOptions
             {
                 IsLocked = request.IsLocked,
                 Name = request.Name,
                 ParentId = request.ParentId,
                 ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList()
-            });
 
-            var item = task.Result;
+            }).ConfigureAwait(false);
 
             var dto = _dtoService.GetBaseItemDto(item, new List<ItemFields>());
 
@@ -90,9 +92,4 @@ namespace MediaBrowser.Api.Movies
             Task.WaitAll(task);
         }
     }
-
-    public class CollectionCreationResult
-    {
-        public string Id { get; set; }
-    }
 }

+ 2 - 0
MediaBrowser.Api/Movies/MoviesService.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -64,6 +65,7 @@ namespace MediaBrowser.Api.Movies
     /// <summary>
     /// Class MoviesService
     /// </summary>
+    [Authenticated]
     public class MoviesService : BaseApiService
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Api/Movies/TrailersService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using ServiceStack;
 
@@ -18,6 +19,7 @@ namespace MediaBrowser.Api.Movies
     /// <summary>
     /// Class TrailersService
     /// </summary>
+    [Authenticated]
     public class TrailersService : BaseApiService
     {
         /// <summary>

+ 2 - 1
MediaBrowser.Api/Music/AlbumsService.cs

@@ -2,10 +2,10 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using ServiceStack;
 using System;
-using System.Collections.Generic;
 using System.Linq;
 
 namespace MediaBrowser.Api.Music
@@ -15,6 +15,7 @@ namespace MediaBrowser.Api.Music
     {
     }
 
+    [Authenticated]
     public class AlbumsService : BaseApiService
     {
         /// <summary>

+ 38 - 0
MediaBrowser.Api/Music/InstantMixService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System.Collections.Generic;
@@ -33,6 +34,21 @@ namespace MediaBrowser.Api.Music
         public string Name { get; set; }
     }
 
+    [Route("/Artists/InstantMix", "GET", Summary = "Creates an instant playlist based on a given artist")]
+    public class GetInstantMixFromArtistId : BaseGetSimilarItems
+    {
+        [ApiMember(Name = "Id", Description = "The artist Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
+    public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
+    {
+        [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "querypath", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Authenticated]
     public class InstantMixService : BaseApiService
     {
         private readonly IUserManager _userManager;
@@ -49,6 +65,28 @@ namespace MediaBrowser.Api.Music
             _libraryManager = libraryManager;
         }
 
+        public object Get(GetInstantMixFromArtistId request)
+        {
+            var item = (MusicArtist)_libraryManager.GetItemById(request.Id);
+
+            var user = _userManager.GetUserById(request.UserId.Value);
+
+            var items = _musicManager.GetInstantMixFromArtist(item.Name, user);
+
+            return GetResult(items, user, request);
+        }
+
+        public object Get(GetInstantMixFromMusicGenreId request)
+        {
+            var item = (MusicGenre)_libraryManager.GetItemById(request.Id);
+
+            var user = _userManager.GetUserById(request.UserId.Value);
+
+            var items = _musicManager.GetInstantMixFromGenres(new[] { item.Name }, user);
+
+            return GetResult(items, user, request);
+        }
+
         public object Get(GetInstantMixFromSong request)
         {
             var item = (Audio)_libraryManager.GetItemById(request.Id);

+ 2 - 0
MediaBrowser.Api/NotificationsService.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Notifications;
 using ServiceStack;
@@ -82,6 +83,7 @@ namespace MediaBrowser.Api
         public string Ids { get; set; }
     }
 
+    [Authenticated]
     public class NotificationsService : BaseApiService
     {
         private readonly INotificationsRepository _notificationsRepo;

+ 2 - 0
MediaBrowser.Api/PackageReviewService.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Constants;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Serialization;
 using ServiceStack;
@@ -96,6 +97,7 @@ namespace MediaBrowser.Api
 
     }
 
+    [Authenticated]
     public class PackageReviewService : BaseApiService
     {
         private readonly IHttpClient _httpClient;

+ 2 - 0
MediaBrowser.Api/PackageService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Updates;
 using ServiceStack;
 using System;
@@ -121,6 +122,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class PackageService
     /// </summary>
+    [Authenticated]
     public class PackageService : BaseApiService
     {
         private readonly IInstallationManager _installationManager;

+ 185 - 112
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -13,7 +13,6 @@ using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Library;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using System;
@@ -123,7 +122,11 @@ namespace MediaBrowser.Api.Playback
 
             var outputFileExtension = GetOutputFileExtension(state);
 
-            return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower());
+            var data = GetCommandLineArguments("dummy\\dummy", state, false);
+
+            data += "-" + (state.Request.DeviceId ?? string.Empty);
+
+            return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower());
         }
 
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
@@ -138,14 +141,9 @@ namespace MediaBrowser.Api.Playback
         {
             var time = request.StartTimeTicks;
 
-            if (time.HasValue)
+            if (time.HasValue && time.Value > 0)
             {
-                var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds;
-
-                if (seconds > 0)
-                {
-                    return string.Format("-ss {0}", seconds.ToString(UsCulture));
-                }
+                return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
             }
 
             return string.Empty;
@@ -319,7 +317,7 @@ namespace MediaBrowser.Api.Playback
                 switch (qualitySetting)
                 {
                     case EncodingQuality.HighSpeed:
-                        param = "-preset ultrafast";
+                        param = "-preset superfast";
                         break;
                     case EncodingQuality.HighQuality:
                         param = "-preset superfast";
@@ -350,16 +348,16 @@ namespace MediaBrowser.Api.Playback
                 var profileScore = 0;
 
                 string crf;
+                var qmin = "0";
+                var qmax = "50";
 
                 switch (qualitySetting)
                 {
                     case EncodingQuality.HighSpeed:
-                        crf = "12";
-                        profileScore = 2;
+                        crf = "10";
                         break;
                     case EncodingQuality.HighQuality:
-                        crf = "8";
-                        profileScore = 1;
+                        crf = "6";
                         break;
                     case EncodingQuality.MaxQuality:
                         crf = "4";
@@ -371,14 +369,17 @@ namespace MediaBrowser.Api.Playback
                 if (isVc1)
                 {
                     profileScore++;
-                    // Max of 2
-                    profileScore = Math.Min(profileScore, 2);
                 }
 
+                // Max of 2
+                profileScore = Math.Min(profileScore, 2);
+
                 // http://www.webmproject.org/docs/encoder-parameters/
-                param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1}",
+                param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
                     profileScore.ToString(UsCulture),
-                    crf);
+                    crf,
+                    qmin,
+                    qmax);
             }
 
             else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase))
@@ -469,11 +470,11 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <param name="state">The state.</param>
         /// <param name="outputVideoCodec">The output video codec.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
         /// <returns>System.String.</returns>
         protected string GetOutputSizeParam(StreamState state,
             string outputVideoCodec,
-            CancellationToken cancellationToken)
+            bool allowTimeStampCopy = true)
         {
             // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
 
@@ -562,11 +563,14 @@ namespace MediaBrowser.Api.Playback
 
             if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
             {
-                var subParam = GetTextSubtitleParam(state, cancellationToken);
+                var subParam = GetTextSubtitleParam(state);
 
                 filters.Add(subParam);
 
-                output += " -copyts";
+                if (allowTimeStampCopy)
+                {
+                    output += " -copyts";
+                }
             }
 
             if (filters.Count > 0)
@@ -581,12 +585,10 @@ namespace MediaBrowser.Api.Playback
         /// Gets the text subtitle param.
         /// </summary>
         /// <param name="state">The state.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>System.String.</returns>
-        protected string GetTextSubtitleParam(StreamState state,
-            CancellationToken cancellationToken)
+        protected string GetTextSubtitleParam(StreamState state)
         {
-            var seconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds;
+            var seconds = Math.Round(TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds);
 
             if (state.SubtitleStream.IsExternal)
             {
@@ -604,17 +606,17 @@ namespace MediaBrowser.Api.Playback
                     }
                 }
 
-                // TODO: Perhaps also use original_size=1920x800
+                // TODO: Perhaps also use original_size=1920x800 ??
                 return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
                     subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
                     charsetParam,
-                    Math.Round(seconds).ToString(UsCulture));
+                    seconds.ToString(UsCulture));
             }
 
             return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
                 state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
                 state.InternalSubtitleStreamOffset.ToString(UsCulture),
-                Math.Round(seconds).ToString(UsCulture));
+                seconds.ToString(UsCulture));
         }
 
         /// <summary>
@@ -623,7 +625,7 @@ namespace MediaBrowser.Api.Playback
         /// <param name="state">The state.</param>
         /// <param name="outputVideoCodec">The output video codec.</param>
         /// <returns>System.String.</returns>
-        protected string GetInternalGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
+        protected string GetGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
         {
             var outputSizeParam = string.Empty;
 
@@ -632,7 +634,7 @@ namespace MediaBrowser.Api.Playback
             // Add resolution params, if specified
             if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
             {
-                outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, CancellationToken.None).TrimEnd('"');
+                outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
                 outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
             }
 
@@ -772,6 +774,11 @@ namespace MediaBrowser.Api.Playback
             return "copy";
         }
 
+        protected virtual bool SupportsThrottling
+        {
+            get { return false; }
+        }
+
         /// <summary>
         /// Gets the input argument.
         /// </summary>
@@ -779,6 +786,19 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         protected string GetInputArgument(StreamState state)
         {
+            if (state.InputProtocol == MediaProtocol.File &&
+               state.RunTimeTicks.HasValue &&
+               state.VideoType == VideoType.VideoFile &&
+               !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
+                {
+                    var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
+
+                    return string.Format("\"{0}\"", url);
+                }
+            }
+
             var protocol = state.InputProtocol;
 
             var inputPath = new[] { state.MediaPath };
@@ -794,6 +814,81 @@ namespace MediaBrowser.Api.Playback
             return MediaEncoder.GetInputArgument(inputPath, protocol);
         }
 
+        private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+        {
+            if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
+            {
+                state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
+            }
+
+            if (string.IsNullOrEmpty(state.MediaPath))
+            {
+                var checkCodecs = false;
+
+                if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
+                {
+                    var streamInfo = await LiveTvManager.GetChannelStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
+
+                    state.LiveTvStreamId = streamInfo.Id;
+
+                    if (!string.IsNullOrEmpty(streamInfo.Path))
+                    {
+                        state.MediaPath = streamInfo.Path;
+                        state.InputProtocol = MediaProtocol.File;
+
+                        await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                    else if (!string.IsNullOrEmpty(streamInfo.Url))
+                    {
+                        state.MediaPath = streamInfo.Url;
+                        state.InputProtocol = MediaProtocol.Http;
+                    }
+
+                    AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl);
+                    checkCodecs = true;
+                }
+
+                else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
+                    string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
+                {
+                    var streamInfo = await LiveTvManager.GetRecordingStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
+
+                    state.LiveTvStreamId = streamInfo.Id;
+
+                    if (!string.IsNullOrEmpty(streamInfo.Path))
+                    {
+                        state.MediaPath = streamInfo.Path;
+                        state.InputProtocol = MediaProtocol.File;
+
+                        await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                    else if (!string.IsNullOrEmpty(streamInfo.Url))
+                    {
+                        state.MediaPath = streamInfo.Url;
+                        state.InputProtocol = MediaProtocol.Http;
+                    }
+
+                    AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl);
+                    checkCodecs = true;
+                }
+
+                var videoRequest = state.VideoRequest;
+
+                if (videoRequest != null && checkCodecs)
+                {
+                    if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
+                    {
+                        state.OutputVideoCodec = "copy";
+                    }
+
+                    if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
+                    {
+                        state.OutputAudioCodec = "copy";
+                    }
+                }
+            }
+        }
+
         /// <summary>
         /// Starts the FFMPEG.
         /// </summary>
@@ -811,10 +906,7 @@ namespace MediaBrowser.Api.Playback
 
             Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
 
-            if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
-            {
-                state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
+            await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 
             var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
 
@@ -849,7 +941,6 @@ namespace MediaBrowser.Api.Playback
             ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
                 TranscodingJobType,
                 process,
-                state.Request.StartTimeTicks,
                 state.Request.DeviceId,
                 state,
                 cancellationTokenSource);
@@ -866,7 +957,7 @@ namespace MediaBrowser.Api.Playback
             var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
             await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
 
-            process.Exited += (sender, args) => OnFfMpegProcessExited(process, state);
+            process.Exited += (sender, args) => OnFfMpegProcessExited(process, state, outputPath);
 
             try
             {
@@ -892,18 +983,6 @@ namespace MediaBrowser.Api.Playback
             {
                 await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
             }
-
-            // Allow a small amount of time to buffer a little
-            if (state.IsInputVideo)
-            {
-                await Task.Delay(500, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
-
-            // This is arbitrary, but add a little buffer time when internet streaming
-            if (state.InputProtocol != MediaProtocol.File)
-            {
-                await Task.Delay(3000, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
         }
 
         private async void StartStreamingLog(StreamState state, Stream source, Stream target)
@@ -1061,7 +1140,8 @@ namespace MediaBrowser.Api.Playback
                 // Make sure we don't request a bitrate higher than the source
                 var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
 
-                return Math.Min(currentBitrate, request.AudioBitRate.Value);
+                return request.AudioBitRate.Value;
+                //return Math.Min(currentBitrate, request.AudioBitRate.Value);
             }
 
             return null;
@@ -1091,8 +1171,16 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <param name="process">The process.</param>
         /// <param name="state">The state.</param>
-        private void OnFfMpegProcessExited(Process process, StreamState state)
+        /// <param name="outputPath">The output path.</param>
+        private void OnFfMpegProcessExited(Process process, StreamState state, string outputPath)
         {
+            var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType);
+
+            if (job != null)
+            {
+                job.HasExited = true;
+            }
+
             Logger.Debug("Disposing stream resources");
             state.Dispose();
 
@@ -1126,13 +1214,13 @@ namespace MediaBrowser.Api.Playback
                     return state.VideoRequest.Framerate.Value;
                 }
 
-                var maxrate = state.VideoRequest.MaxFramerate ?? 23.97602;
+                var maxrate = state.VideoRequest.MaxFramerate;
 
-                if (state.VideoStream != null)
+                if (maxrate.HasValue && state.VideoStream != null)
                 {
                     var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
 
-                    if (contentRate.HasValue && contentRate.Value > maxrate)
+                    if (contentRate.HasValue && contentRate.Value > maxrate.Value)
                     {
                         return maxrate;
                     }
@@ -1330,8 +1418,6 @@ namespace MediaBrowser.Api.Playback
                 ParseParams(request);
             }
 
-            var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager);
-
             var url = Request.PathInfo;
 
             if (string.IsNullOrEmpty(request.AudioCodec))
@@ -1353,13 +1439,10 @@ namespace MediaBrowser.Api.Playback
 
             var item = LibraryManager.GetItemById(request.Id);
 
-            if (user != null && item.GetPlayAccess(user) != PlayAccess.Full)
-            {
-                throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name));
-            }
-
             List<MediaStream> mediaStreams = null;
 
+            state.ItemType = item.GetType().Name;
+
             if (item is ILiveTvRecording)
             {
                 var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false);
@@ -1376,16 +1459,8 @@ namespace MediaBrowser.Api.Playback
 
                 mediaStreams = source.MediaStreams;
 
-                if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl))
-                {
-                    var streamInfo = await LiveTvManager.GetRecordingStream(request.Id, cancellationToken).ConfigureAwait(false);
-
-                    state.LiveTvStreamId = streamInfo.Id;
-                    mediaStreams = streamInfo.MediaStreams;
-
-                    path = streamInfo.Path;
-                    mediaUrl = streamInfo.Url;
-                }
+                // Just to prevent this from being null and causing other methods to fail
+                state.MediaPath = string.Empty;
 
                 if (!string.IsNullOrEmpty(path))
                 {
@@ -1397,17 +1472,20 @@ namespace MediaBrowser.Api.Playback
                     state.MediaPath = mediaUrl;
                     state.InputProtocol = MediaProtocol.Http;
                 }
-
-                state.RunTimeTicks = recording.RunTimeTicks;
+                else
+                {
+                    // No media info, so this is probably needed
+                    state.DeInterlace = true;
+                }
 
                 if (recording.RecordingInfo.Status == RecordingStatus.InProgress)
                 {
-                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
+                    state.ReadInputAtNativeFramerate = true;
                 }
 
-                state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress;
+                state.RunTimeTicks = recording.RunTimeTicks;
+
                 state.OutputAudioSync = "1000";
-                state.DeInterlace = true;
                 state.InputVideoSync = "-1";
                 state.InputAudioSync = "1";
                 state.InputContainer = recording.Container;
@@ -1418,40 +1496,27 @@ namespace MediaBrowser.Api.Playback
 
                 state.VideoType = VideoType.VideoFile;
                 state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
-                var streamInfo = await LiveTvManager.GetChannelStream(request.Id, cancellationToken).ConfigureAwait(false);
-
-                state.LiveTvStreamId = streamInfo.Id;
-                mediaStreams = streamInfo.MediaStreams;
-
-                if (!string.IsNullOrEmpty(streamInfo.Path))
-                {
-                    state.MediaPath = streamInfo.Path;
-                    state.InputProtocol = MediaProtocol.File;
-
-                    await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
-                }
-                else if (!string.IsNullOrEmpty(streamInfo.Url))
-                {
-                    state.MediaPath = streamInfo.Url;
-                    state.InputProtocol = MediaProtocol.Http;
-                }
+                mediaStreams = new List<MediaStream>();
 
                 state.ReadInputAtNativeFramerate = true;
                 state.OutputAudioSync = "1000";
                 state.DeInterlace = true;
                 state.InputVideoSync = "-1";
                 state.InputAudioSync = "1";
+
+                // Just to prevent this from being null and causing other methods to fail
+                state.MediaPath = string.Empty;
             }
             else if (item is IChannelMediaItem)
             {
-                var source = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
+                var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
                 state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-                state.InputProtocol = source.Protocol;
-                state.MediaPath = source.Path;
+                state.InputProtocol = mediaSource.Protocol;
+                state.MediaPath = mediaSource.Path;
                 state.RunTimeTicks = item.RunTimeTicks;
-                state.RemoteHttpHeaders = source.RequiredHttpHeaders;
-                mediaStreams = source.MediaStreams;
+                state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+                state.InputBitrate = mediaSource.Bitrate;
+                mediaStreams = mediaSource.MediaStreams;
             }
             else
             {
@@ -1465,6 +1530,7 @@ namespace MediaBrowser.Api.Playback
                 state.MediaPath = mediaSource.Path;
                 state.InputProtocol = mediaSource.Protocol;
                 state.InputContainer = mediaSource.Container;
+                state.InputBitrate = mediaSource.Bitrate;
 
                 if (item is Video)
                 {
@@ -1488,16 +1554,23 @@ namespace MediaBrowser.Api.Playback
                 state.RunTimeTicks = mediaSource.RunTimeTicks;
             }
 
-            if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
+            // If it's a wtv and we don't have media info, we will probably need to deinterlace
+            if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) &&
+                mediaStreams.Count == 0)
             {
                 state.DeInterlace = true;
             }
 
+            if (state.InputProtocol == MediaProtocol.Rtmp)
+            {
+                state.ReadInputAtNativeFramerate = true;
+            }
+
             var videoRequest = request as VideoStreamRequest;
 
             AttachMediaStreamInfo(state, mediaStreams, videoRequest, url);
 
-            state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10;
+            state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 7;
             state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440;
 
             var container = Path.GetExtension(state.RequestedUrl);
@@ -1574,6 +1647,8 @@ namespace MediaBrowser.Api.Playback
             {
                 state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
             }
+
+            state.AllMediaStreams = mediaStreams;
         }
 
         private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
@@ -1609,7 +1684,10 @@ namespace MediaBrowser.Api.Playback
             // Can't stream copy if we're burning in subtitles
             if (request.SubtitleStreamIndex.HasValue)
             {
-                return false;
+                if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
+                {
+                    return false;
+                }
             }
 
             // Source and target codecs must match
@@ -1886,7 +1964,8 @@ namespace MediaBrowser.Api.Playback
                     state.TargetPacketLength,
                     state.TranscodeSeekInfo,
                     state.IsTargetAnamorphic
-                    );
+
+                    ).FirstOrDefault() ?? string.Empty;
             }
 
             foreach (var item in responseHeaders)
@@ -1911,12 +1990,6 @@ namespace MediaBrowser.Api.Playback
         /// <param name="videoRequest">The video request.</param>
         private void EnforceResolutionLimit(StreamState state, VideoStreamRequest videoRequest)
         {
-            // If enabled, allow whatever the client asks for
-            if (ServerConfigurationManager.Configuration.AllowVideoUpscaling)
-            {
-                return;
-            }
-
             // Switch the incoming params to be ceilings rather than fixed values
             videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
             videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
@@ -1925,7 +1998,7 @@ namespace MediaBrowser.Api.Playback
             videoRequest.Height = null;
         }
 
-        protected string GetInputModifier(StreamState state)
+        protected string GetInputModifier(StreamState state, bool genPts = true)
         {
             var inputModifier = string.Empty;
 
@@ -1945,9 +2018,9 @@ namespace MediaBrowser.Api.Playback
             inputModifier += " " + GetFastSeekCommandLineParameter(state.Request);
             inputModifier = inputModifier.Trim();
 
-            if (state.VideoRequest != null)
+            if (state.VideoRequest != null && genPts)
             {
-                inputModifier += " -fflags genpts";
+                inputModifier += " -fflags +genpts";
             }
 
             if (!string.IsNullOrEmpty(state.InputAudioSync))

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

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

+ 0 - 32
MediaBrowser.Api/Playback/EndlessStreamCopy.cs

@@ -1,32 +0,0 @@
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.Playback
-{
-    public class EndlessStreamCopy
-    {
-        public async Task CopyStream(Stream source, Stream target, CancellationToken cancellationToken)
-        {
-            long position = 0;
-            
-            while (!cancellationToken.IsCancellationRequested)
-            {
-                await source.CopyToAsync(target, 81920, cancellationToken).ConfigureAwait(false);
-
-                var fsPosition = source.Position;
-
-                var bytesRead = fsPosition - position;
-
-                //Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path);
-
-                if (bytesRead == 0)
-                {
-                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                }
-
-                position = fsPosition;
-            }
-        }
-    }
-}

+ 68 - 84
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -1,14 +1,11 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.IO;
 using System;
@@ -25,7 +22,8 @@ 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)
+            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
         {
         }
 
@@ -62,29 +60,33 @@ namespace MediaBrowser.Api.Playback.Hls
         /// Processes the request.
         /// </summary>
         /// <param name="request">The request.</param>
+        /// <param name="isLive">if set to <c>true</c> [is live].</param>
         /// <returns>System.Object.</returns>
-        protected object ProcessRequest(StreamRequest request)
+        protected object ProcessRequest(StreamRequest request, bool isLive)
         {
-            return ProcessRequestAsync(request).Result;
+            return ProcessRequestAsync(request, isLive).Result;
         }
 
-        private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
         /// <summary>
         /// Processes the request async.
         /// </summary>
         /// <param name="request">The request.</param>
+        /// <param name="isLive">if set to <c>true</c> [is live].</param>
         /// <returns>Task{System.Object}.</returns>
-        /// <exception cref="ArgumentException">
-        /// A video bitrate is required
+        /// <exception cref="ArgumentException">A video bitrate is required
         /// or
-        /// An audio bitrate is required
-        /// </exception>
-        private async Task<object> ProcessRequestAsync(StreamRequest request)
+        /// An audio bitrate is required</exception>
+        private async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
         {
             var cancellationTokenSource = new CancellationTokenSource();
 
             var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
 
+            if (isLive)
+            {
+                state.Request.StartTimeTicks = null;
+            }
+
             var playlist = state.OutputFilePath;
 
             if (File.Exists(playlist))
@@ -93,7 +95,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
             else
             {
-                await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+                await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
                 try
                 {
                     if (File.Exists(playlist))
@@ -113,18 +115,34 @@ namespace MediaBrowser.Api.Playback.Hls
                             throw;
                         }
 
-                        await WaitForMinimumSegmentCount(playlist, GetSegmentWait(), cancellationTokenSource.Token).ConfigureAwait(false);
+                        var waitCount = isLive ? 1 : GetSegmentWait();
+                        await WaitForMinimumSegmentCount(playlist, waitCount, cancellationTokenSource.Token).ConfigureAwait(false);
                     }
                 }
                 finally
                 {
-                    FfmpegStartLock.Release();
+                    ApiEntryPoint.Instance.TranscodingStartLock.Release();
+                }
+            }
+
+            if (isLive)
+            {
+                //var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
+
+                //file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file);
+
+                try
+                {
+                    return ResultFactory.GetStaticFileResult(Request, playlist, FileShare.ReadWrite);
+                }
+                finally
+                {
+                    ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
                 }
             }
 
-            int audioBitrate;
-            int videoBitrate;
-            GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
+            var audioBitrate = state.OutputAudioBitrate ?? 0;
+            var videoBitrate = state.OutputVideoBitrate ?? 0;
 
             var appendBaselineStream = false;
             var baselineStreamBitrate = 64000;
@@ -165,37 +183,6 @@ namespace MediaBrowser.Api.Playback.Hls
             return minimumSegmentCount;
         }
 
-        /// <summary>
-        /// Gets the playlist bitrates.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="audioBitrate">The audio bitrate.</param>
-        /// <param name="videoBitrate">The video bitrate.</param>
-        protected void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
-        {
-            var audioBitrateParam = state.OutputAudioBitrate;
-            var videoBitrateParam = state.OutputVideoBitrate;
-
-            if (!audioBitrateParam.HasValue)
-            {
-                if (state.AudioStream != null)
-                {
-                    audioBitrateParam = state.AudioStream.BitRate;
-                }
-            }
-
-            if (!videoBitrateParam.HasValue)
-            {
-                if (state.VideoStream != null)
-                {
-                    videoBitrateParam = state.VideoStream.BitRate;
-                }
-            }
-
-            audioBitrate = audioBitrateParam ?? 0;
-            videoBitrate = videoBitrateParam ?? 0;
-        }
-
         private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
         {
             var builder = new StringBuilder();
@@ -223,49 +210,37 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
         {
+            Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
+
             while (true)
             {
-                cancellationToken.ThrowIfCancellationRequested();
-                
-                string fileText;
-
                 // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
                 using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
                 {
                     using (var reader = new StreamReader(fileStream))
                     {
-                        fileText = await reader.ReadToEndAsync().ConfigureAwait(false);
-                    }
-                }
+                        var count = 0;
 
-                if (CountStringOccurrences(fileText, "#EXTINF:") >= segmentCount)
-                {
-                    break;
+                        while (!reader.EndOfStream)
+                        {
+                            var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                            if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+                            {
+                                count++;
+                                if (count >= segmentCount)
+                                {
+                                    Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+                                    return;
+                                }
+                            }
+                        }
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    }
                 }
-
-                await Task.Delay(25, cancellationToken).ConfigureAwait(false);
             }
         }
 
-        /// <summary>
-        /// Count occurrences of strings.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="pattern">The pattern.</param>
-        /// <returns>System.Int32.</returns>
-        private static int CountStringOccurrences(string text, string pattern)
-        {
-            // Loop through all instances of the string 'text'.
-            var count = 0;
-            var i = 0;
-            while ((i = text.IndexOf(pattern, i, StringComparison.OrdinalIgnoreCase)) != -1)
-            {
-                i += pattern.Length;
-                count++;
-            }
-            return count;
-        }
-
         /// <summary>
         /// Gets the command line arguments.
         /// </summary>
@@ -276,10 +251,10 @@ namespace MediaBrowser.Api.Playback.Hls
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
-            
+
             var itsOffsetMs = hlsVideoRequest == null
                                        ? 0
-                                       : ((GetHlsVideoStream)state.VideoRequest).TimeStampOffsetMs;
+                                       : hlsVideoRequest.TimeStampOffsetMs;
 
             var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
 
@@ -290,7 +265,15 @@ namespace MediaBrowser.Api.Playback.Hls
             // If isEncoding is true we're actually starting ffmpeg
             var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
 
-            var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
+            var baseUrlParam = string.Empty;
+
+            if (state.Request is GetLiveHlsStream)
+            {
+                baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
+                    "hls/" + Path.GetFileNameWithoutExtension(outputPath));
+            }
+
+            var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
                 itsOffset,
                 inputModifier,
                 GetInputArgument(state),
@@ -301,6 +284,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 state.SegmentLength.ToString(UsCulture),
                 startNumberParam,
                 state.HlsListSize.ToString(UsCulture),
+                baseUrlParam,
                 outputPath
                 ).Trim();
 

+ 400 - 74
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -1,45 +1,46 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
-    [Route("/Videos/{Id}/master.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    /// <summary>
+    /// Options is needed for chromecast. Threw Head in there since it's related
+    /// </summary>
+    [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
+    [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetMasterHlsVideoStream : VideoStreamRequest
     {
-        [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? BaselineStreamAudioBitRate { get; set; }
+        public bool EnableAdaptiveBitrateStreaming { get; set; }
 
-        [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool AppendBaselineStream { get; set; }
+        public GetMasterHlsVideoStream()
+        {
+            EnableAdaptiveBitrateStreaming = true;
+        }
     }
 
-    [Route("/Videos/{Id}/main.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetMainHlsVideoStream : VideoStreamRequest
     {
     }
 
-    [Route("/Videos/{Id}/baseline.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
-    public class GetBaselineHlsVideoStream : VideoStreamRequest
-    {
-    }
-
     /// <summary>
     /// Class GetHlsVideoSegment
     /// </summary>
@@ -58,34 +59,48 @@ namespace MediaBrowser.Api.Playback.Hls
 
     public class DynamicHlsService : BaseHlsService
     {
-        public DynamicHlsService(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 DynamicHlsService(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 object Get(GetMasterHlsVideoStream request)
         {
-            var result = GetAsync(request).Result;
+            var result = GetAsync(request, "GET").Result;
 
             return result;
         }
 
-        public object Get(GetDynamicHlsVideoSegment request)
+        public object Head(GetMasterHlsVideoStream request)
         {
-            if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
-            {
-                return GetDynamicSegment(request, false).Result;
-            }
+            var result = GetAsync(request, "HEAD").Result;
 
-            return GetDynamicSegment(request, true).Result;
+            return result;
         }
 
-        private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
-        private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
+        public object Get(GetMainHlsVideoStream request)
         {
+            var result = GetPlaylistAsync(request, "main").Result;
+
+            return result;
+        }
+
+        public object Get(GetDynamicHlsVideoSegment request)
+        {
+            return GetDynamicSegment(request, request.SegmentId).Result;
+        }
+
+        private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId)
+        {
+            if ((request.StartTimeTicks ?? 0) > 0)
+            {
+                throw new ArgumentException("StartTimeTicks is not allowed.");
+            }
+
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationToken = cancellationTokenSource.Token;
 
-            var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
+            var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
 
             var state = await GetState(request, cancellationToken).ConfigureAwait(false);
 
@@ -96,25 +111,35 @@ namespace MediaBrowser.Api.Playback.Hls
             if (File.Exists(segmentPath))
             {
                 ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-                return GetSegementResult(segmentPath);
+                return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
             }
 
-            await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             try
             {
                 if (File.Exists(segmentPath))
                 {
                     ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-                    return GetSegementResult(segmentPath);
+                    return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
                 }
                 else
                 {
-                    if (index == 0)
+                    var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath);
+
+                    if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4)
                     {
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
                         {
-                            ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false);
+                            await ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType.Hls && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase), false).ConfigureAwait(false);
+
+                            if (currentTranscodingIndex.HasValue)
+                            {
+                                DeleteLastFile(playlistPath, 0);
+                            }
+
+                            var startSeconds = index * state.SegmentLength;
+                            request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks;
 
                             await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
                         }
@@ -124,13 +149,13 @@ namespace MediaBrowser.Api.Playback.Hls
                             throw;
                         }
 
-                        await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
+                        await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
                     }
                 }
             }
             finally
             {
-                FfmpegStartLock.Release();
+                ApiEntryPoint.Instance.TranscodingStartLock.Release();
             }
 
             Logger.Info("waiting for {0}", segmentPath);
@@ -140,14 +165,88 @@ namespace MediaBrowser.Api.Playback.Hls
             }
 
             Logger.Info("returning {0}", segmentPath);
-            return GetSegementResult(segmentPath);
+            return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
+        }
+
+        public int? GetCurrentTranscodingIndex(string playlist)
+        {
+            var file = GetLastTranscodingFile(playlist, FileSystem);
+
+            if (file == null)
+            {
+                return null;
+            }
+
+            var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+
+            var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+
+            return int.Parse(indexString, NumberStyles.Integer, UsCulture);
+        }
+
+        private void DeleteLastFile(string path, int retryCount)
+        {
+            if (retryCount >= 5)
+            {
+                return;
+            }
+
+            var file = GetLastTranscodingFile(path, FileSystem);
+
+            if (file != null)
+            {
+                try
+                {
+                    File.Delete(file.FullName);
+                }
+                catch (IOException ex)
+                {
+                    Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
+
+                    Thread.Sleep(100);
+                    DeleteLastFile(path, retryCount + 1);
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
+                }
+            }
+        }
+
+        private static FileInfo GetLastTranscodingFile(string playlist, IFileSystem fileSystem)
+        {
+            var folder = Path.GetDirectoryName(playlist);
+
+            try
+            {
+                return new DirectoryInfo(folder)
+                    .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
+                    .Where(i => string.Equals(i.Extension, ".ts", StringComparison.OrdinalIgnoreCase))
+                    .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
+                    .FirstOrDefault();
+            }
+            catch (DirectoryNotFoundException)
+            {
+                return null;
+            }
         }
 
         protected override int GetStartNumber(StreamState state)
         {
-            var request = (GetDynamicHlsVideoSegment) state.Request;
+            return GetStartNumber(state.VideoRequest);
+        }
+
+        private int GetStartNumber(VideoStreamRequest request)
+        {
+            var segmentId = "0";
+
+            var segmentRequest = request as GetDynamicHlsVideoSegment;
+            if (segmentRequest != null)
+            {
+                segmentId = segmentRequest.SegmentId;
+            }
 
-            return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
+            return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
         }
 
         private string GetSegmentPath(string playlist, int index)
@@ -159,75 +258,262 @@ namespace MediaBrowser.Api.Playback.Hls
             return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts");
         }
 
-        private object GetSegementResult(string path)
+        private async Task<object> GetSegmentResult(string playlistPath, string segmentPath, int segmentIndex, CancellationToken cancellationToken)
+        {
+            // If all transcoding has completed, just return immediately
+            if (!IsTranscoding(playlistPath))
+            {
+                return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
+            }
+
+            var segmentFilename = Path.GetFileName(segmentPath);
+
+            using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+            {
+                using (var reader = new StreamReader(fileStream))
+                {
+                    var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+                    // If it appears in the playlist, it's done
+                    if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
+                    {
+                        return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
+                    }
+                }
+            }
+
+            // if a different file is encoding, it's done
+            //var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath);
+            //if (currentTranscodingIndex > segmentIndex)
+            //{
+            //    return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
+            //}
+
+            // Wait for the file to stop being written to, then stream it
+            var length = new FileInfo(segmentPath).Length;
+            var eofCount = 0;
+
+            while (eofCount < 10)
+            {
+                var info = new FileInfo(segmentPath);
+
+                if (!info.Exists)
+                {
+                    break;
+                }
+
+                var newLength = info.Length;
+
+                if (newLength == length)
+                {
+                    eofCount++;
+                }
+                else
+                {
+                    eofCount = 0;
+                }
+
+                length = newLength;
+                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+            }
+
+            return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
+        }
+
+        private bool IsTranscoding(string playlistPath)
         {
-            // TODO: Handle if it's currently being written to
-            return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
+            var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
+
+            return job != null && !job.HasExited;
         }
 
-        private async Task<object> GetAsync(GetMasterHlsVideoStream request)
+        private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method)
         {
             var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
 
-            int audioBitrate;
-            int videoBitrate;
-            GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
+            if (string.Equals(request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                throw new ArgumentException("Audio codec copy is not allowed here.");
+            }
 
-            var appendBaselineStream = false;
-            var baselineStreamBitrate = 64000;
+            if (string.Equals(request.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                throw new ArgumentException("Video codec copy is not allowed here.");
+            }
 
-            var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
-            if (hlsVideoRequest != null)
+            if (string.IsNullOrEmpty(request.MediaSourceId))
             {
-                appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
-                baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
+                throw new ArgumentException("MediaSourceId is required");
             }
 
-            var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
+            var playlistText = string.Empty;
+
+            if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
+            {
+                var audioBitrate = state.OutputAudioBitrate ?? 0;
+                var videoBitrate = state.OutputVideoBitrate ?? 0;
+
+                playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
+            }
 
             return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
 
-        private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
+        private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
         {
             var builder = new StringBuilder();
 
             builder.AppendLine("#EXTM3U");
 
-            // Pad a little to satisfy the apple hls validator
-            var paddedBitrate = Convert.ToInt32(bitrate * 1.05);
-
             var queryStringIndex = Request.RawUrl.IndexOf('?');
             var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
 
             // Main stream
-            builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
-            var playlistUrl = "main.m3u8" + queryString;
-            builder.AppendLine(playlistUrl);
+            var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
+            playlistUrl += queryString;
+
+            var request = (GetMasterHlsVideoStream)state.Request;
+
+            var subtitleStreams = state.AllMediaStreams
+                .Where(i => i.IsTextSubtitleStream)
+                .ToList();
+
+            var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ?
+                "subs" :
+                null;
+
+            AppendPlaylist(builder, playlistUrl, totalBitrate, subtitleGroup);
 
-            // Low bitrate stream
-            if (includeBaselineStream)
+            if (EnableAdaptiveBitrateStreaming(state))
             {
-                builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
-                playlistUrl = "baseline.m3u8" + queryString;
-                builder.AppendLine(playlistUrl);
+                var requestedVideoBitrate = state.VideoRequest.VideoBitRate.Value;
+
+                // By default, vary by just 200k
+                var variation = GetBitrateVariation(totalBitrate);
+
+                var newBitrate = totalBitrate - variation;
+                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, (requestedVideoBitrate - variation));
+                AppendPlaylist(builder, variantUrl, newBitrate, subtitleGroup);
+
+                variation *= 2;
+                newBitrate = totalBitrate - variation;
+                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, (requestedVideoBitrate - variation));
+                AppendPlaylist(builder, variantUrl, newBitrate, subtitleGroup);
+            }
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                AddSubtitles(state, subtitleStreams, builder);
             }
 
             return builder.ToString();
         }
 
-        public object Get(GetMainHlsVideoStream request)
+        private string ReplaceBitrate(string url, int oldValue, int newValue)
         {
-            var result = GetPlaylistAsync(request, "main").Result;
+            return url.Replace(
+                "videobitrate=" + oldValue.ToString(UsCulture),
+                "videobitrate=" + newValue.ToString(UsCulture),
+                StringComparison.OrdinalIgnoreCase);
+        }
 
-            return result;
+        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
+        {
+            var selectedIndex = state.SubtitleStream == null ? (int?)null : state.SubtitleStream.Index;
+
+            foreach (var stream in subtitles)
+            {
+                const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},URI=\"{3}\",LANGUAGE=\"{4}\"";
+
+                var name = stream.Language;
+
+                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+                var isForced = stream.IsForced;
+
+                if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown";
+
+                var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}",
+                    state.Request.MediaSourceId,
+                    stream.Index.ToString(UsCulture),
+                    30.ToString(UsCulture));
+
+                var line = string.Format(format,
+                    name,
+                    isDefault ? "YES" : "NO",
+                    isForced ? "YES" : "NO",
+                    url,
+                    stream.Language ?? "Unknown");
+
+                builder.AppendLine(line);
+            }
         }
 
-        public object Get(GetBaselineHlsVideoStream request)
+        private bool EnableAdaptiveBitrateStreaming(StreamState state)
         {
-            var result = GetPlaylistAsync(request, "baseline").Result;
+            var request = state.Request as GetMasterHlsVideoStream;
 
-            return result;
+            if (request != null && !request.EnableAdaptiveBitrateStreaming)
+            {
+                return false;
+            }
+
+            if (string.IsNullOrWhiteSpace(state.MediaPath))
+            {
+                // Opening live streams is so slow it's not even worth it
+                return false;
+            }
+
+            return state.VideoRequest.VideoBitRate.HasValue;
+        }
+
+        private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup)
+        {
+            var header = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture);
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
+            }
+
+            builder.AppendLine(header);
+            builder.AppendLine(url);
+        }
+
+        private int GetBitrateVariation(int bitrate)
+        {
+            // By default, vary by just 50k
+            var variation = 50000;
+
+            if (bitrate >= 10000000)
+            {
+                variation = 2000000;
+            }
+            else if (bitrate >= 5000000)
+            {
+                variation = 1500000;
+            }
+            else if (bitrate >= 3000000)
+            {
+                variation = 1000000;
+            }
+            else if (bitrate >= 2000000)
+            {
+                variation = 500000;
+            }
+            else if (bitrate >= 1000000)
+            {
+                variation = 300000;
+            }
+            else if (bitrate >= 600000)
+            {
+                variation = 200000;
+            }
+            else if (bitrate >= 400000)
+            {
+                variation = 100000;
+            }
+
+            return variation;
         }
 
         private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
@@ -240,6 +526,7 @@ namespace MediaBrowser.Api.Playback.Hls
             builder.AppendLine("#EXT-X-VERSION:3");
             builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
             builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+            builder.AppendLine("#EXT-X-ALLOW-CACHE:NO");
 
             var queryStringIndex = Request.RawUrl.IndexOf('?');
             var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
@@ -252,7 +539,7 @@ namespace MediaBrowser.Api.Playback.Hls
             {
                 var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
 
-                builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
+                builder.AppendLine("#EXTINF:" + length.ToString(UsCulture) + ",");
 
                 builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}",
 
@@ -312,9 +599,8 @@ namespace MediaBrowser.Api.Playback.Hls
                 return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
             }
 
-            var keyFrameArg = state.ReadInputAtNativeFramerate ?
-                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
-                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                state.SegmentLength.ToString(UsCulture));
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 
@@ -323,18 +609,50 @@ namespace MediaBrowser.Api.Playback.Hls
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec, CancellationToken.None);
+                args += GetOutputSizeParam(state, codec, false);
             }
 
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += GetInternalGraphicalSubtitleParam(state, codec);
+                args += GetGraphicalSubtitleParam(state, codec);
             }
 
             return args;
         }
 
+        /// <summary>
+        /// Gets the command line arguments.
+        /// </summary>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="state">The state.</param>
+        /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
+        /// <returns>System.String.</returns>
+        protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
+        {
+            var threads = GetNumberOfThreads(state, false);
+
+            var inputModifier = GetInputModifier(state);
+
+            // If isEncoding is true we're actually starting ffmpeg
+            var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
+
+            var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
+                inputModifier,
+                GetInputArgument(state),
+                threads,
+                GetMapArgs(state),
+                GetVideoArguments(state),
+                GetAudioArguments(state),
+                state.SegmentLength.ToString(UsCulture),
+                startNumberParam,
+                state.HlsListSize.ToString(UsCulture),
+                outputPath
+                ).Trim();
+
+            return args;
+        }
+
         /// <summary>
         /// Gets the segment file extension.
         /// </summary>
@@ -344,5 +662,13 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             return ".ts";
         }
+
+        protected override TranscodingJobType TranscodingJobType
+        {
+            get
+            {
+                return TranscodingJobType.Hls;
+            }
+        }
     }
 }

+ 3 - 1
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -74,7 +74,9 @@ namespace MediaBrowser.Api.Playback.Hls
 
         public void Delete(StopEncodingProcess request)
         {
-            ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, true);
+            var task = ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, path => true, true);
+
+            Task.WaitAll(task);
         }
 
         /// <summary>

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

@@ -10,7 +10,6 @@ using ServiceStack;
 using System;
 using System.IO;
 using System.Linq;
-using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls
@@ -32,6 +31,12 @@ namespace MediaBrowser.Api.Playback.Hls
         public int TimeStampOffsetMs { get; set; }
     }
 
+    [Route("/Videos/{Id}/live.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetLiveHlsStream : VideoStreamRequest
+    {
+    }
+    
     /// <summary>
     /// Class GetHlsVideoSegment
     /// </summary>
@@ -105,7 +110,12 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.Object.</returns>
         public object Get(GetHlsVideoStream request)
         {
-            return ProcessRequest(request);
+            return ProcessRequest(request, false);
+        }
+
+        public object Get(GetLiveHlsStream request)
+        {
+            return ProcessRequest(request, true);
         }
 
         /// <summary>
@@ -159,9 +169,8 @@ namespace MediaBrowser.Api.Playback.Hls
                 return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
             }
 
-            var keyFrameArg = state.ReadInputAtNativeFramerate ?
-                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" : 
-                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                state.SegmentLength.ToString(UsCulture));
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 
@@ -170,13 +179,13 @@ namespace MediaBrowser.Api.Playback.Hls
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec, CancellationToken.None);
+                args += GetOutputSizeParam(state, codec);
             }
 
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += GetInternalGraphicalSubtitleParam(state, codec);
+                args += GetGraphicalSubtitleParam(state, codec);
             }
 
             return args;

+ 80 - 51
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using ServiceStack.Web;
@@ -26,7 +27,8 @@ 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, IImageProcessor imageProcessor, IHttpClient httpClient)
+            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
         {
             ImageProcessor = imageProcessor;
             HttpClient = httpClient;
@@ -52,23 +54,23 @@ namespace MediaBrowser.Api.Playback.Progressive
             if (isVideoRequest)
             {
                 var videoCodec = state.VideoRequest.VideoCodec;
-                
-                    if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return ".ts";
-                    }
-                    if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return ".ogv";
-                    }
-                    if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return ".webm";
-                    }
-                    if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return ".asf";
-                    }
+
+                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".ts";
+                }
+                if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".ogv";
+                }
+                if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".webm";
+                }
+                if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".asf";
+                }
             }
 
             // Try to infer based on the desired audio codec
@@ -114,7 +116,9 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <returns>Task.</returns>
         protected object ProcessRequest(StreamRequest request, bool isHeadRequest)
         {
-            var state = GetState(request, CancellationToken.None).Result;
+            var cancellationTokenSource = new CancellationTokenSource();
+
+            var state = GetState(request, cancellationTokenSource.Token).Result;
 
             var responseHeaders = new Dictionary<string, string>();
 
@@ -123,13 +127,9 @@ namespace MediaBrowser.Api.Playback.Progressive
             {
                 AddDlnaHeaders(state, responseHeaders, true);
 
-                try
-                {
-                    return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest).Result;
-                }
-                finally
+                using (state)
                 {
-                    state.Dispose();
+                    return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result;
                 }
             }
 
@@ -151,13 +151,24 @@ namespace MediaBrowser.Api.Playback.Progressive
             {
                 var contentType = state.GetMimeType(state.MediaPath);
 
-                try
+                using (state)
                 {
-                    return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
-                }
-                finally
-                {
-                    state.Dispose();
+                    var throttleLimit = state.InputBitrate.HasValue ? (state.InputBitrate.Value / 8) : 0;
+
+                    return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+                    {
+                        ResponseHeaders = responseHeaders,
+                        ContentType = contentType,
+                        IsHeadRequest = isHeadRequest,
+                        Path = state.MediaPath,
+                        Throttle = request.Throttle,
+
+                        // Pad by 20% to play it safe
+                        ThrottleLimit = Convert.ToInt64(1.2 * throttleLimit),
+
+                        // Three minutes
+                        MinThrottlePosition = throttleLimit * 180
+                    });
                 }
             }
 
@@ -168,7 +179,13 @@ namespace MediaBrowser.Api.Playback.Progressive
 
                 try
                 {
-                    return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
+                    return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+                    {
+                        ResponseHeaders = responseHeaders,
+                        ContentType = contentType,
+                        IsHeadRequest = isHeadRequest,
+                        Path = outputPath
+                    });
                 }
                 finally
                 {
@@ -179,7 +196,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Need to start ffmpeg
             try
             {
-                return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
+                return GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result;
             }
             catch
             {
@@ -195,8 +212,9 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <param name="state">The state.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
+        /// <param name="cancellationTokenSource">The cancellation token source.</param>
         /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest)
+        private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
         {
             string useragent = null;
             state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
@@ -205,7 +223,8 @@ namespace MediaBrowser.Api.Playback.Progressive
             {
                 Url = state.MediaPath,
                 UserAgent = useragent,
-                BufferContent = false
+                BufferContent = false,
+                CancellationToken = cancellationTokenSource.Token
             };
 
             var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
@@ -246,8 +265,9 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <param name="state">The state.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
+        /// <param name="cancellationTokenSource">The cancellation token source.</param>
         /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetStreamResult(StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest)
+        private async Task<object> GetStreamResult(StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
         {
             // Use the command line args with a dummy playlist path
             var outputPath = state.OutputFilePath;
@@ -283,27 +303,36 @@ namespace MediaBrowser.Api.Playback.Progressive
                 return streamResult;
             }
 
-            if (!File.Exists(outputPath))
-            {
-                await StartFfMpeg(state, outputPath, new CancellationTokenSource()).ConfigureAwait(false);
-            }
-            else
+            await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            try
             {
-                ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
-                state.Dispose();
-            }
+                if (!File.Exists(outputPath))
+                {
+                    await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
+                }
+                else
+                {
+                    ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                    state.Dispose();
+                }
 
-            var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem);
+                var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+                var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job);
 
-            result.Options["Content-Type"] = contentType;
+                result.Options["Content-Type"] = contentType;
 
-            // Add the response headers to the result object
-            foreach (var item in responseHeaders)
+                // Add the response headers to the result object
+                foreach (var item in responseHeaders)
+                {
+                    result.Options[item.Key] = item.Value;
+                }
+
+                return result;
+            }
+            finally
             {
-                result.Options[item.Key] = item.Value;
+                ApiEntryPoint.Instance.TranscodingStartLock.Release();
             }
-
-            return result;
         }
 
         /// <summary>

+ 26 - 14
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -1,7 +1,7 @@
-using MediaBrowser.Common.IO;
+using System;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Model.Logging;
 using ServiceStack.Web;
-using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
@@ -13,6 +13,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         private string Path { get; set; }
         private ILogger Logger { get; set; }
         private readonly IFileSystem _fileSystem;
+        private readonly TranscodingJob _job;
 
         /// <summary>
         /// The _options
@@ -33,11 +34,12 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <param name="path">The path.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="fileSystem">The file system.</param>
-        public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem)
+        public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem, TranscodingJob job)
         {
             Path = path;
             Logger = logger;
             _fileSystem = fileSystem;
+            _job = job;
         }
 
         /// <summary>
@@ -60,11 +62,12 @@ namespace MediaBrowser.Api.Playback.Progressive
         {
             try
             {
-                await StreamFile(Path, responseStream).ConfigureAwait(false);
+                await new ProgressiveFileCopier(_fileSystem, _job)
+                    .StreamFile(Path, responseStream).ConfigureAwait(false);
             }
-            catch
+            catch (Exception ex)
             {
-                Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
+                Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
 
                 throw;
             }
@@ -73,14 +76,20 @@ namespace MediaBrowser.Api.Playback.Progressive
                 ApiEntryPoint.Instance.OnTranscodeEndRequest(Path, TranscodingJobType.Progressive);
             }
         }
+    }
 
-        /// <summary>
-        /// Streams the file.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="outputStream">The output stream.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        private async Task StreamFile(string path, Stream outputStream)
+    public class ProgressiveFileCopier
+    {
+        private readonly IFileSystem _fileSystem;
+        private readonly TranscodingJob _job;
+
+        public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job)
+        {
+            _fileSystem = fileSystem;
+            _job = job;
+        }
+
+        public async Task StreamFile(string path, Stream outputStream)
         {
             var eofCount = 0;
             long position = 0;
@@ -99,7 +108,10 @@ namespace MediaBrowser.Api.Playback.Progressive
 
                     if (bytesRead == 0)
                     {
-                        eofCount++;
+                        if (_job == null || _job.HasExited)
+                        {
+                            eofCount++;
+                        }
                         await Task.Delay(100).ConfigureAwait(false);
                     }
                     else

+ 5 - 5
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
 using System.IO;
-using System.Threading;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
@@ -144,7 +143,8 @@ namespace MediaBrowser.Api.Playback.Progressive
                 return state.VideoStream != null && IsH264(state.VideoStream) ? args + " -bsf h264_mp4toannexb" : args;
             }
 
-            const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                5.ToString(UsCulture));
 
             args += keyFrameArg;
 
@@ -153,7 +153,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
             {
-                args += GetOutputSizeParam(state, codec, CancellationToken.None);
+                args += GetOutputSizeParam(state, codec);
             }
 
             var qualityParam = GetVideoQualityParam(state, codec, false);
@@ -166,7 +166,7 @@ namespace MediaBrowser.Api.Playback.Progressive
             // This is for internal graphical subs
             if (hasGraphicalSubs)
             {
-                args += GetInternalGraphicalSubtitleParam(state, codec);
+                args += GetGraphicalSubtitleParam(state, codec);
             }
 
             return args;
@@ -210,7 +210,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, true);
+            args += " " + GetAudioFilterParam(state, false);
 
             return args;
         }

+ 8 - 2
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -1,4 +1,5 @@
-using ServiceStack;
+using MediaBrowser.Model.Dlna;
+using ServiceStack;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -33,7 +34,7 @@ namespace MediaBrowser.Api.Playback
         /// <value>The start time ticks.</value>
         [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public long? StartTimeTicks { get; set; }
-
+        
         /// <summary>
         /// Gets or sets the audio bit rate.
         /// </summary>
@@ -69,6 +70,8 @@ namespace MediaBrowser.Api.Playback
         public string DeviceProfileId { get; set; }
 
         public string Params { get; set; }
+
+        public bool Throttle { get; set; }
     }
 
     public class VideoStreamRequest : StreamRequest
@@ -160,6 +163,9 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string Level { get; set; }
 
+        [ApiMember(Name = "SubtitleDeliveryMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+        
         /// <summary>
         /// Gets a value indicating whether this instance has fixed resolution.
         /// </summary>

+ 9 - 0
MediaBrowser.Api/Playback/StreamState.cs

@@ -38,6 +38,8 @@ namespace MediaBrowser.Api.Playback
 
         public string InputContainer { get; set; }
 
+        public List<MediaStream> AllMediaStreams { get; set; }
+        
         public MediaStream AudioStream { get; set; }
         public MediaStream VideoStream { get; set; }
         public MediaStream SubtitleStream { get; set; }
@@ -66,6 +68,8 @@ namespace MediaBrowser.Api.Playback
 
         public long? RunTimeTicks;
 
+        public long? InputBitrate { get; set; }
+
         public string OutputAudioSync = "1";
         public string OutputVideoSync = "vfr";
 
@@ -78,6 +82,7 @@ namespace MediaBrowser.Api.Playback
             SupportedAudioCodecs = new List<string>();
             PlayableStreamFileNames = new List<string>();
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            AllMediaStreams = new List<MediaStream>();
         }
 
         public string InputAudioSync { get; set; }
@@ -94,6 +99,10 @@ namespace MediaBrowser.Api.Playback
         public bool EnableMpegtsM2TsMode { get; set; }
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
 
+        public long? EncodingDurationTicks { get; set; }
+
+        public string ItemType { get; set; }
+
         public string GetMimeType(string outputPath)
         {
             if (!string.IsNullOrEmpty(MimeType))

+ 176 - 0
MediaBrowser.Api/PlaylistService.cs

@@ -0,0 +1,176 @@
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Playlists;
+using MediaBrowser.Model.Querying;
+using ServiceStack;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    [Route("/Playlists", "POST", Summary = "Creates a new playlist")]
+    public class CreatePlaylist : IReturn<PlaylistCreationResult>
+    {
+        [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Name { get; set; }
+
+        [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
+        public string Ids { get; set; }
+
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string UserId { get; set; }
+
+        [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string MediaType { get; set; }
+    }
+
+    [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")]
+    public class AddToPlaylist : IReturnVoid
+    {
+        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Ids { get; set; }
+
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { get; set; }
+    }
+
+    [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
+    public class RemoveFromPlaylist : IReturnVoid
+    {
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string EntryIds { get; set; }
+    }
+
+    [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
+    public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
+    {
+        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { 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; }
+
+        /// <summary>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Fields { get; set; }
+    }
+
+    [Authenticated]
+    public class PlaylistService : BaseApiService
+    {
+        private readonly IPlaylistManager _playlistManager;
+        private readonly IDtoService _dtoService;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+
+        public PlaylistService(IDtoService dtoService, IPlaylistManager playlistManager, IUserManager userManager, ILibraryManager libraryManager)
+        {
+            _dtoService = dtoService;
+            _playlistManager = playlistManager;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+        }
+
+        public async Task<object> Post(CreatePlaylist request)
+        {
+            var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
+            {
+                Name = request.Name,
+                ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(),
+                UserId = request.UserId,
+                MediaType = request.MediaType
+
+            }).ConfigureAwait(false);
+
+            return ToOptimizedResult(result);
+        }
+
+        public void Post(AddToPlaylist request)
+        {
+            var task = _playlistManager.AddToPlaylist(request.Id, request.Ids.Split(','), request.UserId);
+
+            Task.WaitAll(task);
+        }
+
+        public void Delete(RemoveFromPlaylist request)
+        {
+            var task = _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
+
+            Task.WaitAll(task);
+        }
+
+        public object Get(GetPlaylistItems request)
+        {
+            var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
+            var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(new Guid(request.UserId)) : null;
+
+            var items = playlist.GetManageableItems().ToArray();
+
+            var count = items.Length;
+
+            if (request.StartIndex.HasValue)
+            {
+                items = items.Skip(request.StartIndex.Value).ToArray();
+            }
+
+            if (request.Limit.HasValue)
+            {
+                items = items.Take(request.Limit.Value).ToArray();
+            }
+
+            var dtos = items
+                   .Select(i => _dtoService.GetBaseItemDto(i.Item2, request.GetItemFields().ToList(), user))
+                   .ToArray();
+
+            var index = 0;
+            foreach (var item in dtos)
+            {
+                item.PlaylistItemId = items[index].Item1.Id;
+                index++;
+            }
+
+            var result = new ItemsResult
+            {
+                Items = dtos,
+                TotalRecordCount = count
+            };
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 2 - 0
MediaBrowser.Api/PluginService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Security;
 using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Serialization;
@@ -100,6 +101,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class PluginsService
     /// </summary>
+    [Authenticated]
     public class PluginService : BaseApiService
     {
         /// <summary>

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Tasks;
 using ServiceStack;
 using ServiceStack.Text.Controller;
@@ -78,6 +79,7 @@ namespace MediaBrowser.Api.ScheduledTasks
     /// <summary>
     /// Class ScheduledTasksService
     /// </summary>
+    [Authenticated]
     public class ScheduledTaskService : BaseApiService
     {
         /// <summary>

+ 6 - 4
MediaBrowser.Api/SearchService.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Search;
 using ServiceStack;
@@ -79,6 +80,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class SearchService
     /// </summary>
+    [Authenticated]
     public class SearchService : BaseApiService
     {
         /// <summary>
@@ -109,9 +111,9 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        public object Get(GetSearchHints request)
+        public async Task<object> Get(GetSearchHints request)
         {
-            var result = GetSearchHintsAsync(request).Result;
+            var result = await GetSearchHintsAsync(request).ConfigureAwait(false);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -192,14 +194,14 @@ namespace MediaBrowser.Api
             {
                 result.Series = season.Series.Name;
 
-                result.EpisodeCount = season.GetRecursiveChildren(i => i is Episode).Count;
+                result.EpisodeCount = season.GetRecursiveChildren().Count(i => i is Episode);
             }
 
             var series = item as Series;
 
             if (series != null)
             {
-                result.EpisodeCount = series.GetRecursiveChildren(i => i is Episode).Count;
+                result.EpisodeCount = series.GetRecursiveChildren().Count(i => i is Episode);
             }
 
             var album = item as MusicAlbum;

+ 1 - 1
MediaBrowser.Api/WebSocket/SessionInfoWebSocketListener.cs → MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs

@@ -7,7 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api.WebSocket
+namespace MediaBrowser.Api.Session
 {
     /// <summary>
     /// Class SessionInfoWebSocketListener

+ 96 - 22
MediaBrowser.Api/SessionsService.cs → MediaBrowser.Api/Session/SessionsService.cs

@@ -1,4 +1,6 @@
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Session;
 using ServiceStack;
@@ -8,12 +10,13 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Session
 {
     /// <summary>
     /// Class GetSessions
     /// </summary>
     [Route("/Sessions", "GET", Summary = "Gets a list of sessions")]
+    [Authenticated]
     public class GetSessions : IReturn<List<SessionInfoDto>>
     {
         [ApiMember(Name = "ControllableByUserId", Description = "Optional. Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
@@ -27,6 +30,7 @@ namespace MediaBrowser.Api
     /// Class DisplayContent
     /// </summary>
     [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
+    [Authenticated]
     public class DisplayContent : IReturnVoid
     {
         /// <summary>
@@ -59,6 +63,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")]
+    [Authenticated]
     public class Play : IReturnVoid
     {
         /// <summary>
@@ -91,6 +96,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")]
+    [Authenticated]
     public class SendPlaystateCommand : IReturnVoid
     {
         /// <summary>
@@ -115,6 +121,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")]
+    [Authenticated]
     public class SendSystemCommand : IReturnVoid
     {
         /// <summary>
@@ -133,6 +140,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")]
+    [Authenticated]
     public class SendGeneralCommand : IReturnVoid
     {
         /// <summary>
@@ -151,6 +159,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")]
+    [Authenticated]
     public class SendFullGeneralCommand : GeneralCommand, IReturnVoid
     {
         /// <summary>
@@ -162,6 +171,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")]
+    [Authenticated]
     public class SendMessageCommand : IReturnVoid
     {
         /// <summary>
@@ -182,6 +192,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")]
+    [Authenticated]
     public class AddUserToSession : IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
@@ -192,6 +203,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")]
+    [Authenticated]
     public class RemoveUserFromSession : IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
@@ -202,7 +214,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
-    [Route("/Sessions/{Id}/Capabilities", "POST", Summary = "Updates capabilities for a device")]
+    [Authenticated]
     public class PostCapabilities : IReturnVoid
     {
         /// <summary>
@@ -225,6 +237,30 @@ namespace MediaBrowser.Api
         public bool SupportsMediaControl { get; set; }
     }
 
+    [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
+    public class ReportSessionEnded : IReturnVoid
+    {
+    }
+
+    [Route("/Auth/Keys", "GET")]
+    public class GetApiKeys
+    {
+    }
+
+    [Route("/Auth/Keys/{Key}", "DELETE")]
+    public class RevokeKey
+    {
+        [ApiMember(Name = "Key", Description = "Auth Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Key { get; set; }
+    }
+
+    [Route("/Auth/Keys", "POST")]
+    public class CreateKey
+    {
+        [ApiMember(Name = "App", Description = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string App { get; set; }
+    }
+
     /// <summary>
     /// Class SessionsService
     /// </summary>
@@ -236,16 +272,60 @@ namespace MediaBrowser.Api
         private readonly ISessionManager _sessionManager;
 
         private readonly IUserManager _userManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IAuthenticationRepository _authRepo;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionsService" /> class.
         /// </summary>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="userManager">The user manager.</param>
-        public SessionsService(ISessionManager sessionManager, IUserManager userManager)
+        /// <param name="authContext">The authentication context.</param>
+        /// <param name="authRepo">The authentication repo.</param>
+        public SessionsService(ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, IAuthenticationRepository authRepo)
         {
             _sessionManager = sessionManager;
             _userManager = userManager;
+            _authContext = authContext;
+            _authRepo = authRepo;
+        }
+
+        public void Delete(RevokeKey request)
+        {
+            var task = _sessionManager.RevokeToken(request.Key);
+
+            Task.WaitAll(task);
+        }
+
+        public void Post(CreateKey request)
+        {
+            var task = _authRepo.Create(new AuthenticationInfo
+            {
+                AppName = request.App,
+                IsActive = true,
+                AccessToken = Guid.NewGuid().ToString("N"),
+                DateCreated = DateTime.UtcNow
+
+            }, CancellationToken.None);
+
+            Task.WaitAll(task);
+        }
+
+        public void Post(ReportSessionEnded request)
+        {
+            var auth = _authContext.GetAuthorizationInfo(Request);
+
+            _sessionManager.Logout(auth.Token);
+        }
+
+        public object Get(GetApiKeys request)
+        {
+            var result = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                IsActive = true
+            });
+
+            return ToOptimizedResult(result);
         }
 
         /// <summary>
@@ -315,21 +395,24 @@ namespace MediaBrowser.Api
         public void Post(SendSystemCommand request)
         {
             GeneralCommandType commandType;
+            var name = request.Command;
 
-            if (Enum.TryParse(request.Command, true, out commandType))
+            if (Enum.TryParse(name, true, out commandType))
             {
-                var currentSession = GetSession();
+                name = commandType.ToString();
+            }
 
-                var command = new GeneralCommand
-                {
-                    Name = commandType.ToString(),
-                    ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
-                };
+            var currentSession = GetSession();
 
-                var task = _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
+            var command = new GeneralCommand
+            {
+                Name = name,
+                ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
+            };
 
-                Task.WaitAll(task);
-            }
+            var task = _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
+
+            Task.WaitAll(task);
         }
 
         /// <summary>
@@ -422,14 +505,5 @@ namespace MediaBrowser.Api
                 MessageCallbackUrl = request.MessageCallbackUrl
             });
         }
-
-        private SessionInfo GetSession()
-        {
-            var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
-
-            return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) &&
-                string.Equals(i.Client, auth.Client) &&
-                string.Equals(i.ApplicationVersion, auth.Version));
-        }
     }
 }

+ 2 - 2
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -78,8 +78,8 @@ namespace MediaBrowser.Api
             var fields = request.GetItemFields().ToList();
 
             var inputItems = user == null
-                                 ? libraryManager.RootFolder.GetRecursiveChildren(i => i.Id != item.Id)
-                                 : user.RootFolder.GetRecursiveChildren(user, i => i.Id != item.Id);
+                                 ? libraryManager.RootFolder.GetRecursiveChildren().Where(i => i.Id != item.Id)
+                                 : user.RootFolder.GetRecursiveChildren(user).Where(i => i.Id != item.Id);
 
             var items = GetSimilaritems(item, inputItems.Where(includeInSearch), getSimilarityScore)
                 .ToList();

+ 116 - 31
MediaBrowser.Api/Library/SubtitleService.cs → MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
@@ -8,37 +9,17 @@ using MediaBrowser.Model.Providers;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api.Library
+namespace MediaBrowser.Api.Subtitles
 {
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format (vtt).")]
-    public class GetSubtitle
-    {
-        /// <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; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-
-        [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Format { get; set; }
-
-        [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public long StartPositionTicks { get; set; }
-    }
-
     [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
+    [Authenticated]
     public class DeleteSubtitle
     {
         /// <summary>
@@ -53,6 +34,7 @@ namespace MediaBrowser.Api.Library
     }
 
     [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
+    [Authenticated]
     public class SearchRemoteSubtitles : IReturn<List<RemoteSubtitleInfo>>
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
@@ -63,6 +45,7 @@ namespace MediaBrowser.Api.Library
     }
 
     [Route("/Items/{Id}/RemoteSearch/Subtitles/Providers", "GET")]
+    [Authenticated]
     public class GetSubtitleProviders : IReturn<List<SubtitleProviderInfo>>
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
@@ -70,6 +53,7 @@ namespace MediaBrowser.Api.Library
     }
 
     [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
+    [Authenticated]
     public class DownloadRemoteSubtitles : IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
@@ -80,12 +64,60 @@ namespace MediaBrowser.Api.Library
     }
 
     [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
+    [Authenticated]
     public class GetRemoteSubtitles : IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
     }
 
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
+    public class GetSubtitle
+    {
+        /// <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; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string MediaSourceId { get; set; }
+
+        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int Index { get; set; }
+
+        [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Format { get; set; }
+
+        [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public long StartPositionTicks { get; set; }
+
+        [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public long? EndPositionTicks { get; set; }
+    }
+
+    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
+    public class GetSubtitlePlaylist
+    {
+        /// <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; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string MediaSourceId { get; set; }
+
+        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int Index { get; set; }
+
+        [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int SegmentLength { get; set; }
+    }
+
     public class SubtitleService : BaseApiService
     {
         private readonly ILibraryManager _libraryManager;
@@ -99,16 +131,59 @@ namespace MediaBrowser.Api.Library
             _subtitleEncoder = subtitleEncoder;
         }
 
-        public object Get(SearchRemoteSubtitles request)
+        public object Get(GetSubtitlePlaylist request)
         {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
+            var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
 
-            var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
+            var mediaSource = item.GetMediaSources(false)
+                .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
 
-            return ToOptimizedResult(response);
+            var builder = new StringBuilder();
+
+            var runtime = mediaSource.RunTimeTicks ?? -1;
+
+            if (runtime <= 0)
+            {
+                throw new ArgumentException("HLS Subtitles are not supported for this media.");
+            }
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+            long positionTicks = 0;
+            var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
+
+            while (positionTicks < runtime)
+            {
+                var remaining = runtime - positionTicks;
+                var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+
+                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+
+                var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+
+                var url = string.Format("stream.srt?StartPositionTicks={0}&EndPositionTicks={1}",
+                    positionTicks.ToString(CultureInfo.InvariantCulture),
+                    endPositionTicks.ToString(CultureInfo.InvariantCulture));
+
+                builder.AppendLine(url);
+
+                positionTicks += segmentLengthTicks;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+
+            return ResultFactory.GetResult(builder.ToString(), Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
+
         public object Get(GetSubtitle request)
         {
+            if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
+            {
+                request.Format = "json";
+            }
             if (string.IsNullOrEmpty(request.Format))
             {
                 var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
@@ -129,14 +204,24 @@ namespace MediaBrowser.Api.Library
 
         private async Task<Stream> GetSubtitles(GetSubtitle request)
         {
-            return await _subtitleEncoder.GetSubtitles(request.Id, 
-                request.MediaSourceId, 
-                request.Index, 
+            return await _subtitleEncoder.GetSubtitles(request.Id,
+                request.MediaSourceId,
+                request.Index,
                 request.Format,
                 request.StartPositionTicks,
+                request.EndPositionTicks,
                 CancellationToken.None).ConfigureAwait(false);
         }
 
+        public object Get(SearchRemoteSubtitles request)
+        {
+            var video = (Video)_libraryManager.GetItemById(request.Id);
+
+            var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
+
+            return ToOptimizedResult(response);
+        }
+
         public void Delete(DeleteSubtitle request)
         {
             var task = _subtitleManager.DeleteSubtitles(request.Id, request.Index);

+ 104 - 0
MediaBrowser.Api/Sync/SyncService.cs

@@ -0,0 +1,104 @@
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Sync;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Sync;
+using ServiceStack;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Sync
+{
+    [Route("/Sync/Jobs/{Id}", "DELETE", Summary = "Cancels a sync job.")]
+    public class CancelSyncJob : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sync/Jobs/{Id}", "GET", Summary = "Gets a sync job.")]
+    public class GetSyncJob : IReturn<SyncJob>
+    {
+        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sync/Jobs", "GET", Summary = "Gets sync jobs.")]
+    public class GetSyncJobs : IReturn<QueryResult<SyncJob>>
+    {
+        /// <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("/Sync/Jobs", "POST", Summary = "Gets sync jobs.")]
+    public class CreateSyncJob : SyncJobRequest, IReturn<SyncJobCreationResult>
+    {
+    }
+
+    [Route("/Sync/Targets", "GET", Summary = "Gets a list of available sync targets.")]
+    public class GetSyncTarget : IReturn<List<SyncTarget>>
+    {
+        [ApiMember(Name = "UserId", Description = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string UserId { get; set; }
+    }
+
+    [Authenticated]
+    public class SyncService : BaseApiService
+    {
+        private readonly ISyncManager _syncManager;
+
+        public SyncService(ISyncManager syncManager)
+        {
+            _syncManager = syncManager;
+        }
+
+        public object Get(GetSyncTarget request)
+        {
+            var result = _syncManager.GetSyncTargets(request.UserId);
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetSyncJobs request)
+        {
+            var result = _syncManager.GetJobs(new SyncJobQuery
+            {
+                 StartIndex = request.StartIndex,
+                 Limit = request.Limit
+            });
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetSyncJob request)
+        {
+            var result = _syncManager.GetJob(request.Id);
+
+            return ToOptimizedResult(result);
+        }
+
+        public void Delete(CancelSyncJob request)
+        {
+            var task = _syncManager.CancelJob(request.Id);
+
+            Task.WaitAll(task);
+        }
+
+        public async Task<object> Post(CreateSyncJob request)
+        {
+            var result = await _syncManager.CreateJob(request).ConfigureAwait(false);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 53 - 0
MediaBrowser.Api/System/ActivityLogService.cs

@@ -0,0 +1,53 @@
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Querying;
+using ServiceStack;
+using System;
+using System.Globalization;
+
+namespace MediaBrowser.Api.System
+{
+    [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
+    public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
+    {
+        /// <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; }
+
+        [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string MinDate { get; set; }
+    }
+
+    [Authenticated]
+    public class ActivityLogService : BaseApiService
+    {
+        private readonly IActivityManager _activityManager;
+
+        public ActivityLogService(IActivityManager activityManager)
+        {
+            _activityManager = activityManager;
+        }
+
+        public object Get(GetActivityLogs request)
+        {
+            DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
+                (DateTime?)null :
+                DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
+
+            var result = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 67 - 0
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs

@@ -0,0 +1,67 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.System
+{
+    /// <summary>
+    /// Class SessionInfoWebSocketListener
+    /// </summary>
+    class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        protected override string Name
+        {
+            get { return "ActivityLogEntry"; }
+        }
+
+        /// <summary>
+        /// The _kernel
+        /// </summary>
+        private readonly IActivityManager _activityManager;
+
+        public ActivityLogWebSocketListener(ILogger logger, IActivityManager activityManager)
+            : base(logger)
+        {
+            _activityManager = activityManager;
+            _activityManager.EntryCreated += _activityManager_EntryCreated;
+        }
+
+        void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        {
+            SendData(true);
+        }
+
+        /// <summary>
+        /// Gets the data to send.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>Task{SystemInfo}.</returns>
+        protected override Task<List<ActivityLogEntry>> GetDataToSend(WebSocketListenerState state)
+        {
+            return Task.FromResult(new List<ActivityLogEntry>());
+        }
+
+        protected override bool SendOnTimer
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        protected override void Dispose(bool dispose)
+        {
+            _activityManager.EntryCreated -= _activityManager_EntryCreated;
+
+            base.Dispose(dispose);
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Api/WebSocket/SystemInfoWebSocketListener.cs → MediaBrowser.Api/System/SystemInfoWebSocketListener.cs

@@ -4,7 +4,7 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.System;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api.WebSocket
+namespace MediaBrowser.Api.System
 {
     /// <summary>
     /// Class SystemInfoWebSocketListener

+ 205 - 0
MediaBrowser.Api/System/SystemService.cs

@@ -0,0 +1,205 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.System;
+using ServiceStack;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.System
+{
+    /// <summary>
+    /// Class GetSystemInfo
+    /// </summary>
+    [Route("/System/Info", "GET", Summary = "Gets information about the server")]
+    [Authenticated]
+    public class GetSystemInfo : IReturn<SystemInfo>
+    {
+
+    }
+
+    [Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")]
+    public class GetPublicSystemInfo : IReturn<PublicSystemInfo>
+    {
+
+    }
+
+    /// <summary>
+    /// Class RestartApplication
+    /// </summary>
+    [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
+    [Authenticated]
+    public class RestartApplication
+    {
+    }
+
+    /// <summary>
+    /// This is currently not authenticated because the uninstaller needs to be able to shutdown the server.
+    /// </summary>
+    [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
+    public class ShutdownApplication
+    {
+    }
+
+    [Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")]
+    [Authenticated]
+    public class GetServerLogs : IReturn<List<LogFile>>
+    {
+    }
+
+    [Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")]
+    [Authenticated]
+    public class GetEndpointInfo : IReturn<EndpointInfo>
+    {
+        public string Endpoint { get; set; }
+    }
+
+    [Route("/System/Logs/Log", "GET", Summary = "Gets a log file")]
+    public class GetLogFile
+    {
+        [ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class SystemInfoService
+    /// </summary>
+    public class SystemService : BaseApiService
+    {
+        /// <summary>
+        /// The _app host
+        /// </summary>
+        private readonly IServerApplicationHost _appHost;
+        private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+
+        private readonly INetworkManager _network;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SystemService" /> class.
+        /// </summary>
+        /// <param name="appHost">The app host.</param>
+        /// <param name="appPaths">The application paths.</param>
+        /// <param name="fileSystem">The file system.</param>
+        /// <exception cref="ArgumentNullException">jsonSerializer</exception>
+        public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem, INetworkManager network)
+        {
+            _appHost = appHost;
+            _appPaths = appPaths;
+            _fileSystem = fileSystem;
+            _network = network;
+        }
+
+        public object Get(GetServerLogs request)
+        {
+            List<FileInfo> files;
+
+            try
+            {
+                files = new DirectoryInfo(_appPaths.LogDirectoryPath)
+                    .EnumerateFiles("*", SearchOption.AllDirectories)
+                    .Where(i => string.Equals(i.Extension, ".txt", global::System.StringComparison.OrdinalIgnoreCase))
+                    .ToList();
+            }
+            catch (DirectoryNotFoundException)
+            {
+                files = new List<FileInfo>();
+            }
+
+            var result = files.Select(i => new LogFile
+            {
+                DateCreated = _fileSystem.GetCreationTimeUtc(i),
+                DateModified = _fileSystem.GetLastWriteTimeUtc(i),
+                Name = i.Name,
+                Size = i.Length
+
+            }).OrderByDescending(i => i.DateModified)
+                .ThenByDescending(i => i.DateCreated)
+                .ThenBy(i => i.Name)
+                .ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetLogFile request)
+        {
+            var file = new DirectoryInfo(_appPaths.LogDirectoryPath)
+                .EnumerateFiles("*", SearchOption.AllDirectories)
+                .First(i => string.Equals(i.Name, request.Name, global::System.StringComparison.OrdinalIgnoreCase));
+
+            return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSystemInfo request)
+        {
+            var result = _appHost.GetSystemInfo();
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetPublicSystemInfo request)
+        {
+            var result = _appHost.GetSystemInfo();
+
+            var publicInfo = new PublicSystemInfo
+            {
+                Id = result.Id,
+                ServerName = result.ServerName,
+                Version = result.Version
+            };
+
+            return ToOptimizedResult(publicInfo);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(RestartApplication request)
+        {
+            Task.Run(async () =>
+            {
+                await Task.Delay(100).ConfigureAwait(false);
+                await _appHost.Restart().ConfigureAwait(false);
+            });
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(ShutdownApplication request)
+        {
+            Task.Run(async () =>
+            {
+                await Task.Delay(100).ConfigureAwait(false);
+                await _appHost.Shutdown().ConfigureAwait(false);
+            });
+        }
+
+        public object Get(GetEndpointInfo request)
+        {
+            return ToOptimizedResult(new EndpointInfo
+            {
+                IsLocal = Request.IsLocal,
+                IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp)
+            });
+        }
+    }
+
+    public class EndpointInfo
+    {
+        public bool IsLocal { get; set; }
+        public bool IsInNetwork { get; set; }
+    }
+}

+ 0 - 90
MediaBrowser.Api/SystemService.cs

@@ -1,90 +0,0 @@
-using MediaBrowser.Controller;
-using MediaBrowser.Model.System;
-using ServiceStack;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetSystemInfo
-    /// </summary>
-    [Route("/System/Info", "GET", Summary = "Gets information about the server")]
-    public class GetSystemInfo : IReturn<SystemInfo>
-    {
-
-    }
-
-    /// <summary>
-    /// Class RestartApplication
-    /// </summary>
-    [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
-    public class RestartApplication
-    {
-    }
-
-    [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
-    public class ShutdownApplication
-    {
-    }
-
-    /// <summary>
-    /// Class SystemInfoService
-    /// </summary>
-    public class SystemService : BaseApiService
-    {
-        /// <summary>
-        /// The _app host
-        /// </summary>
-        private readonly IServerApplicationHost _appHost;
-
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SystemService" /> class.
-        /// </summary>
-        /// <param name="appHost">The app host.</param>
-        /// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
-        public SystemService(IServerApplicationHost appHost)
-        {
-            _appHost = appHost;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSystemInfo request)
-        {
-            var result = _appHost.GetSystemInfo();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RestartApplication request)
-        {
-            Task.Run(async () =>
-            {
-                await Task.Delay(100).ConfigureAwait(false);
-                await _appHost.Restart().ConfigureAwait(false);
-            });
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(ShutdownApplication request)
-        {
-            Task.Run(async () =>
-            {
-                await Task.Delay(100).ConfigureAwait(false);
-                await _appHost.Shutdown().ConfigureAwait(false);
-            });
-        }
-
-    }
-}

+ 2 - 0
MediaBrowser.Api/TvShowsService.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -175,6 +176,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class TvShowsService
     /// </summary>
+    [Authenticated]
     public class TvShowsService : BaseApiService
     {
         /// <summary>

+ 4 - 2
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -46,6 +47,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class ArtistsService
     /// </summary>
+    [Authenticated]
     public class ArtistsService : BaseItemsByNameService<MusicArtist>
     {
         /// <summary>
@@ -88,10 +90,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

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

@@ -1,9 +1,9 @@
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
 using System;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace MediaBrowser.Api.UserLibrary
 {

+ 4 - 2
MediaBrowser.Api/UserLibrary/GameGenresService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -41,6 +42,7 @@ namespace MediaBrowser.Api.UserLibrary
         public Guid? UserId { get; set; }
     }
 
+    [Authenticated]
     public class GameGenresService : BaseItemsByNameService<GameGenre>
     {
         public GameGenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
@@ -76,10 +78,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

+ 4 - 2
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -46,6 +47,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GenresService
     /// </summary>
+    [Authenticated]
     public class GenresService : BaseItemsByNameService<Genre>
     {
         public GenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
@@ -81,10 +83,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

+ 3 - 1
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -246,6 +247,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class ItemsService
     /// </summary>
+    [Authenticated]
     public class ItemsService : BaseApiService
     {
         /// <summary>
@@ -1428,7 +1430,7 @@ namespace MediaBrowser.Api.UserLibrary
                 nextId = list[index + 1].Id;
             }
 
-            return list.Where(i => i.Id == previousId || i.Id == nextId);
+            return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid);
         }
 
         /// <summary>

+ 4 - 2
MediaBrowser.Api/UserLibrary/MusicGenresService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -41,6 +42,7 @@ namespace MediaBrowser.Api.UserLibrary
         public Guid? UserId { get; set; }
     }
 
+    [Authenticated]
     public class MusicGenresService : BaseItemsByNameService<MusicGenre>
     {
         public MusicGenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
@@ -76,10 +78,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

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

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -51,6 +52,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class PersonsService
     /// </summary>
+    [Authenticated]
     public class PersonsService : BaseItemsByNameService<Person>
     {
         /// <summary>
@@ -93,10 +95,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

+ 388 - 0
MediaBrowser.Api/UserLibrary/PlaystateService.cs

@@ -0,0 +1,388 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
+using ServiceStack;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class MarkPlayedItem
+    /// </summary>
+    [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")]
+    [Api(Description = "Marks an item as played")]
+    public class MarkPlayedItem : IReturn<UserItemDataDto>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public Guid UserId { get; set; }
+
+        [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string DatePlayed { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class MarkUnplayedItem
+    /// </summary>
+    [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")]
+    [Api(Description = "Marks an item as unplayed")]
+    public class MarkUnplayedItem : IReturn<UserItemDataDto>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Sessions/Playing", "POST")]
+    [Api(Description = "Reports playback has started within a session")]
+    public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
+    {
+    }
+
+    [Route("/Sessions/Playing/Progress", "POST")]
+    [Api(Description = "Reports playback progress within a session")]
+    public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
+    {
+    }
+
+    [Route("/Sessions/Playing/Stopped", "POST")]
+    [Api(Description = "Reports playback has stopped within a session")]
+    public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
+    {
+    }
+
+    /// <summary>
+    /// Class OnPlaybackStart
+    /// </summary>
+    [Route("/Users/{UserId}/PlayingItems/{Id}", "POST")]
+    [Api(Description = "Reports that a user has begun playing an item")]
+    public class OnPlaybackStart : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
+        /// </summary>
+        /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
+        [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public bool CanSeek { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
+        public string QueueableMediaTypes { get; set; }
+
+        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? AudioStreamIndex { get; set; }
+
+        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? SubtitleStreamIndex { get; set; }
+    }
+
+    /// <summary>
+    /// Class OnPlaybackProgress
+    /// </summary>
+    [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")]
+    [Api(Description = "Reports a user's playback progress")]
+    public class OnPlaybackProgress : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public long? PositionTicks { get; set; }
+
+        [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public bool IsPaused { get; set; }
+
+        [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
+        public bool IsMuted { get; set; }
+
+        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? AudioStreamIndex { get; set; }
+
+        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? SubtitleStreamIndex { get; set; }
+
+        [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int? VolumeLevel { get; set; }
+    }
+
+    /// <summary>
+    /// Class OnPlaybackStopped
+    /// </summary>
+    [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")]
+    [Api(Description = "Reports that a user has stopped playing an item")]
+    public class OnPlaybackStopped : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string MediaSourceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
+        public long? PositionTicks { get; set; }
+    }
+
+    [Authenticated]
+    public class PlaystateService : BaseApiService
+    {
+        private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataRepository;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ISessionManager _sessionManager;
+
+        public PlaystateService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+            _sessionManager = sessionManager;
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public object Post(MarkPlayedItem request)
+        {
+            var result = MarkPlayed(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        private async Task<UserItemDataDto> MarkPlayed(MarkPlayedItem request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            DateTime? datePlayed = null;
+
+            if (!string.IsNullOrEmpty(request.DatePlayed))
+            {
+                datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
+            }
+
+            var session = GetSession();
+
+            var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
+
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
+
+                await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false);
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(OnPlaybackStart request)
+        {
+            var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty);
+
+            Post(new ReportPlaybackStart
+            {
+                CanSeek = request.CanSeek,
+                ItemId = request.Id,
+                QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
+                MediaSourceId = request.MediaSourceId,
+                AudioStreamIndex = request.AudioStreamIndex,
+                SubtitleStreamIndex = request.SubtitleStreamIndex
+            });
+        }
+
+        public void Post(ReportPlaybackStart request)
+        {
+            request.SessionId = GetSession().Id;
+
+            var task = _sessionManager.OnPlaybackStart(request);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(OnPlaybackProgress request)
+        {
+            Post(new ReportPlaybackProgress
+            {
+                ItemId = request.Id,
+                PositionTicks = request.PositionTicks,
+                IsMuted = request.IsMuted,
+                IsPaused = request.IsPaused,
+                MediaSourceId = request.MediaSourceId,
+                AudioStreamIndex = request.AudioStreamIndex,
+                SubtitleStreamIndex = request.SubtitleStreamIndex,
+                VolumeLevel = request.VolumeLevel
+            });
+        }
+
+        public void Post(ReportPlaybackProgress request)
+        {
+            request.SessionId = GetSession().Id;
+
+            var task = _sessionManager.OnPlaybackProgress(request);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(OnPlaybackStopped request)
+        {
+            Post(new ReportPlaybackStopped
+            {
+                ItemId = request.Id,
+                PositionTicks = request.PositionTicks,
+                MediaSourceId = request.MediaSourceId
+            });
+        }
+
+        public void Post(ReportPlaybackStopped request)
+        {
+            request.SessionId = GetSession().Id;
+
+            var task = _sessionManager.OnPlaybackStopped(request);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public object Delete(MarkUnplayedItem request)
+        {
+            var task = MarkUnplayed(request);
+
+            return ToOptimizedResult(task.Result);
+        }
+
+        private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var session = GetSession();
+
+            var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
+
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
+
+                await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false);
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Updates the played status.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+        /// <param name="datePlayed">The date played.</param>
+        /// <returns>Task.</returns>
+        private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+
+            if (wasPlayed)
+            {
+                await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false);
+            }
+            else
+            {
+                await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false);
+            }
+
+            return _userDataRepository.GetUserDataDto(item, user);
+        }
+    }
+}

+ 4 - 2
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -43,6 +44,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class StudiosService
     /// </summary>
+    [Authenticated]
     public class StudiosService : BaseItemsByNameService<Studio>
     {
         public StudiosService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
@@ -78,10 +80,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

+ 137 - 384
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -3,16 +3,14 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Session;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -188,260 +186,97 @@ namespace MediaBrowser.Api.UserLibrary
     }
 
     /// <summary>
-    /// Class MarkPlayedItem
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")]
-    [Api(Description = "Marks an item as played")]
-    public class MarkPlayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string DatePlayed { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class MarkUnplayedItem
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")]
-    [Api(Description = "Marks an item as unplayed")]
-    public class MarkUnplayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/Playing", "POST")]
-    [Api(Description = "Reports playback has started within a session")]
-    public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Progress", "POST")]
-    [Api(Description = "Reports playback progress within a session")]
-    public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Stopped", "POST")]
-    [Api(Description = "Reports playback has stopped within a session")]
-    public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class OnPlaybackStart
+    /// Class GetLocalTrailers
     /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "POST")]
-    [Api(Description = "Reports that a user has begun playing an item")]
-    public class OnPlaybackStart : IReturnVoid
+    [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
+    [Api(Description = "Gets local trailers for an item")]
+    public class GetLocalTrailers : IReturn<List<BaseItemDto>>
     {
         /// <summary>
         /// Gets or sets the user id.
         /// </summary>
         /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public Guid UserId { get; set; }
 
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
         /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
-        /// </summary>
-        /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool CanSeek { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string QueueableMediaTypes { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
     }
 
     /// <summary>
-    /// Class OnPlaybackProgress
+    /// Class GetSpecialFeatures
     /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")]
-    [Api(Description = "Reports a user's playback progress")]
-    public class OnPlaybackProgress : IReturnVoid
+    [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
+    [Api(Description = "Gets special features for an item")]
+    public class GetSpecialFeatures : IReturn<List<BaseItemDto>>
     {
         /// <summary>
         /// Gets or sets the user id.
         /// </summary>
         /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public Guid UserId { get; set; }
 
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
         /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public long? PositionTicks { get; set; }
-
-        [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsPaused { get; set; }
-
-        [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsMuted { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? VolumeLevel { get; set; }
     }
 
-    /// <summary>
-    /// Class OnPlaybackStopped
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")]
-    [Api(Description = "Reports that a user has stopped playing an item")]
-    public class OnPlaybackStopped : IReturnVoid
+    [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")]
+    public class GetLatestMedia : IReturn<List<BaseItemDto>>, IHasItemFields
     {
         /// <summary>
         /// Gets or sets the user id.
         /// </summary>
         /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public Guid UserId { get; set; }
 
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
+        [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int Limit { get; set; }
 
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string MediaSourceId { get; set; }
+        [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>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
-        public long? PositionTicks { get; set; }
-    }
+        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Fields { get; set; }
 
-    /// <summary>
-    /// Class GetLocalTrailers
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
-    [Api(Description = "Gets local trailers for an item")]
-    public class GetLocalTrailers : IReturn<List<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </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; }
+        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string IncludeItemTypes { get; set; }
 
-        /// <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; }
-    }
+        [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsFolder { get; set; }
 
-    /// <summary>
-    /// Class GetSpecialFeatures
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
-    [Api(Description = "Gets special features for an item")]
-    public class GetSpecialFeatures : IReturn<List<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </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; }
+        [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsPlayed { get; set; }
 
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
+        [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool GroupItems { get; set; }
+        
+        public GetLatestMedia()
+        {
+            Limit = 20;
+            GroupItems = true;
+        }
     }
 
-
     /// <summary>
     /// Class UserLibraryService
     /// </summary>
+    [Authenticated]
     public class UserLibraryService : BaseApiService
     {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
         private readonly IUserManager _userManager;
-        /// <summary>
-        /// The _user data repository
-        /// </summary>
         private readonly IUserDataManager _userDataRepository;
-        /// <summary>
-        /// The _library manager
-        /// </summary>
         private readonly ILibraryManager _libraryManager;
-
-        private readonly ISessionManager _sessionManager;
         private readonly IDtoService _dtoService;
-
         private readonly IUserViewManager _userViewManager;
 
         /// <summary>
@@ -450,15 +285,14 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="userManager">The user manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
-        /// <param name="sessionManager">The session manager.</param>
         /// <param name="dtoService">The dto service.</param>
+        /// <param name="userViewManager">The user view manager.</param>
         /// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
-        public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, ISessionManager sessionManager, IDtoService dtoService, IUserViewManager userViewManager)
+        public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
             _userDataRepository = userDataRepository;
-            _sessionManager = sessionManager;
             _dtoService = dtoService;
             _userViewManager = userViewManager;
         }
@@ -475,7 +309,98 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
-        public object Get(GetUserViews request)
+        public object Get(GetLatestMedia request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            // Avoid implicitly captured closure
+            var libraryItems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId)
+                .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)
+            {
+                // 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;
+                }
+            }
+
+            var fields = request.GetItemFields().ToList();
+
+            var dtos = list.Select(i =>
+            {
+                var item = i.Item2[0];
+                var childCount = 0;
+
+                if (i.Item1 != null && i.Item2.Count > 0)
+                {
+                    item = i.Item1;
+                    childCount = i.Item2.Count;
+                }
+
+                var dto = _dtoService.GetBaseItemDto(item, fields, user);
+
+                dto.ChildCount = childCount;
+
+                return dto;
+            });
+
+            return ToOptimizedResult(dtos.ToList());
+        }
+
+        public async Task<object> Get(GetUserViews request)
         {
             var user = _userManager.GetUserById(new Guid(request.UserId));
 
@@ -493,10 +418,9 @@ namespace MediaBrowser.Api.UserLibrary
                 query.IncludeExternalContent = request.IncludeExternalContent.Value;
             }
 
-            var folders = _userViewManager.GetUserViews(query, CancellationToken.None).Result;
+            var folders = await _userViewManager.GetUserViews(query, CancellationToken.None).ConfigureAwait(false);
 
-            var dtos = folders.OrderBy(i => i.SortName)
-                .Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+            var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
                 .ToArray();
 
             var result = new QueryResult<BaseItemDto>
@@ -541,7 +465,8 @@ namespace MediaBrowser.Api.UserLibrary
             if (series != null)
             {
                 var dtos = series
-                    .GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
+                    .GetRecursiveChildren()
+                    .Where(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
                     .OrderBy(i =>
                     {
                         if (i.PremiereDate.HasValue)
@@ -714,9 +639,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false);
 
-            data = _userDataRepository.GetUserData(user.Id, key);
-
-            return _dtoService.GetUserItemDataDto(data);
+            return _userDataRepository.GetUserDataDto(item, user);
         }
 
         /// <summary>
@@ -763,177 +686,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false);
 
-            data = _userDataRepository.GetUserData(user.Id, key);
-
-            return _dtoService.GetUserItemDataDto(data);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(MarkPlayedItem request)
-        {
-            var result = MarkPlayed(request).Result;
-
-            return ToOptimizedResult(result);
-        }
-
-        private async Task<UserItemDataDto> MarkPlayed(MarkPlayedItem request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            DateTime? datePlayed = null;
-
-            if (!string.IsNullOrEmpty(request.DatePlayed))
-            {
-                datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
-            }
-
-            var session = GetSession(_sessionManager);
-
-            var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
-
-                await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false);
-            }
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackStart request)
-        {
-            var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty);
-
-            Post(new ReportPlaybackStart
-            {
-                CanSeek = request.CanSeek,
-                ItemId = request.Id,
-                QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex
-            });
-        }
-
-        public void Post(ReportPlaybackStart request)
-        {
-            request.SessionId = GetSession(_sessionManager).Id;
-
-            var task = _sessionManager.OnPlaybackStart(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackProgress request)
-        {
-            Post(new ReportPlaybackProgress
-            {
-                ItemId = request.Id,
-                PositionTicks = request.PositionTicks,
-                IsMuted = request.IsMuted,
-                IsPaused = request.IsPaused,
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex,
-                VolumeLevel = request.VolumeLevel
-            });
-        }
-
-        public void Post(ReportPlaybackProgress request)
-        {
-            request.SessionId = GetSession(_sessionManager).Id;
-
-            var task = _sessionManager.OnPlaybackProgress(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(OnPlaybackStopped request)
-        {
-            Post(new ReportPlaybackStopped
-            {
-                ItemId = request.Id,
-                PositionTicks = request.PositionTicks,
-                MediaSourceId = request.MediaSourceId
-            });
-        }
-
-        public void Post(ReportPlaybackStopped request)
-        {
-            request.SessionId = GetSession(_sessionManager).Id;
-
-            var task = _sessionManager.OnPlaybackStopped(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(MarkUnplayedItem request)
-        {
-            var task = MarkUnplayed(request);
-
-            return ToOptimizedResult(task.Result);
-        }
-
-        private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var session = GetSession(_sessionManager);
-
-            var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
-
-                await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false);
-            }
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Updates the played status.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
-        /// <param name="datePlayed">The date played.</param>
-        /// <returns>Task.</returns>
-        private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
-        {
-            var item = _libraryManager.GetItemById(itemId);
-
-            if (wasPlayed)
-            {
-                await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false);
-            }
-            else
-            {
-                await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false);
-            }
-
-            return _dtoService.GetUserItemDataDto(_userDataRepository.GetUserData(user.Id, item.GetUserDataKey()));
+            return _userDataRepository.GetUserDataDto(item, user);
         }
     }
 }

+ 4 - 2
MediaBrowser.Api/UserLibrary/YearsService.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -43,6 +44,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class YearsService
     /// </summary>
+    [Authenticated]
     public class YearsService : BaseItemsByNameService<Year>
     {
         public YearsService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
@@ -78,10 +80,10 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var user = UserManager.GetUserById(request.UserId.Value);
 
-                return DtoService.GetItemByNameDto(item, fields.ToList(), user);
+                return DtoService.GetBaseItemDto(item, fields.ToList(), user);
             }
 
-            return DtoService.GetItemByNameDto(item, fields.ToList());
+            return DtoService.GetBaseItemDto(item, fields.ToList());
         }
 
         /// <summary>

+ 112 - 78
MediaBrowser.Api/UserService.cs

@@ -1,9 +1,11 @@
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 using ServiceStack;
 using ServiceStack.Text.Controller;
@@ -18,6 +20,7 @@ namespace MediaBrowser.Api
     /// Class GetUsers
     /// </summary>
     [Route("/Users", "GET", Summary = "Gets a list of users")]
+    [Authenticated]
     public class GetUsers : IReturn<List<UserDto>>
     {
         [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
@@ -36,6 +39,7 @@ namespace MediaBrowser.Api
     /// Class GetUser
     /// </summary>
     [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
+    [Authenticated]
     public class GetUser : IReturn<UserDto>
     {
         /// <summary>
@@ -50,6 +54,7 @@ namespace MediaBrowser.Api
     /// Class DeleteUser
     /// </summary>
     [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")]
+    [Authenticated]
     public class DeleteUser : IReturnVoid
     {
         /// <summary>
@@ -106,6 +111,7 @@ namespace MediaBrowser.Api
     /// Class UpdateUserPassword
     /// </summary>
     [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")]
+    [Authenticated]
     public class UpdateUserPassword : IReturnVoid
     {
         /// <summary>
@@ -137,6 +143,7 @@ namespace MediaBrowser.Api
     /// Class UpdateUser
     /// </summary>
     [Route("/Users/{Id}", "POST", Summary = "Updates a user")]
+    [Authenticated]
     public class UpdateUser : UserDto, IReturnVoid
     {
     }
@@ -145,6 +152,7 @@ namespace MediaBrowser.Api
     /// Class CreateUser
     /// </summary>
     [Route("/Users", "POST", Summary = "Creates a user")]
+    [Authenticated]
     public class CreateUser : UserDto, IReturn<UserDto>
     {
     }
@@ -152,48 +160,68 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class UsersService
     /// </summary>
-    public class UserService : BaseApiService
+    public class UserService : BaseApiService, IHasAuthorization
     {
-        /// <summary>
-        /// The _XML serializer
-        /// </summary>
-        private readonly IXmlSerializer _xmlSerializer;
-
         /// <summary>
         /// The _user manager
         /// </summary>
         private readonly IUserManager _userManager;
         private readonly IDtoService _dtoService;
         private readonly ISessionManager _sessionMananger;
+        private readonly IServerConfigurationManager _config;
+        private readonly INetworkManager _networkManager;
+
+        public IAuthorizationContext AuthorizationContext { get; set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UserService" /> class.
         /// </summary>
-        /// <param name="xmlSerializer">The XML serializer.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="dtoService">The dto service.</param>
+        /// <param name="sessionMananger">The session mananger.</param>
         /// <exception cref="System.ArgumentNullException">xmlSerializer</exception>
-        public UserService(IXmlSerializer xmlSerializer, IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger)
-            : base()
+        public UserService(IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager)
         {
-            if (xmlSerializer == null)
-            {
-                throw new ArgumentNullException("xmlSerializer");
-            }
-
-            _xmlSerializer = xmlSerializer;
             _userManager = userManager;
             _dtoService = dtoService;
             _sessionMananger = sessionMananger;
+            _config = config;
+            _networkManager = networkManager;
         }
 
         public object Get(GetPublicUsers request)
         {
+            var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
+            var isDashboard = string.Equals(authInfo.Client, "Dashboard", StringComparison.OrdinalIgnoreCase);
+
+            if ((Request.IsLocal && isDashboard) ||
+                !_config.Configuration.IsStartupWizardCompleted)
+            {
+                return Get(new GetUsers
+                {
+                    IsDisabled = false
+                });
+            }
+
+            // TODO: Uncomment this once all clients can handle an empty user list.
             return Get(new GetUsers
             {
                 IsHidden = false,
                 IsDisabled = false
             });
+
+            //// TODO: Add or is authenticated
+            //if (Request.IsLocal || IsInLocalNetwork(Request.RemoteIp))
+            //{
+            //    return Get(new GetUsers
+            //    {
+            //        IsHidden = false,
+            //        IsDisabled = false
+            //    });
+            //}
+
+            //// Return empty when external
+            //return ToOptimizedResult(new List<UserDto>());
         }
 
         /// <summary>
@@ -217,7 +245,7 @@ namespace MediaBrowser.Api
 
             var result = users
                 .OrderBy(u => u.Name)
-                .Select(_dtoService.GetUserDto)
+                .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
                 .ToList();
 
             return ToOptimizedSerializedResultUsingCache(result);
@@ -237,7 +265,7 @@ namespace MediaBrowser.Api
                 throw new ResourceNotFoundException("User not found");
             }
 
-            var result = _dtoService.GetUserDto(user);
+            var result = _userManager.GetUserDto(user, Request.RemoteIp);
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
@@ -247,6 +275,13 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         public void Delete(DeleteUser request)
+        {
+            var task = DeleteAsync(request);
+
+            Task.WaitAll(task);
+        }
+
+        public async Task DeleteAsync(DeleteUser request)
         {
             var user = _userManager.GetUserById(request.Id);
 
@@ -255,9 +290,8 @@ namespace MediaBrowser.Api
                 throw new ResourceNotFoundException("User not found");
             }
 
-            var task = _userManager.DeleteUser(user);
-
-            Task.WaitAll(task);
+            await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false);
+            await _userManager.DeleteUser(user).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -266,67 +300,54 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public object Post(AuthenticateUser request)
         {
-            // No response needed. Will throw an exception on failure.
-            var result = AuthenticateUser(request).Result;
-
-            return result;
-        }
-
-        public object Post(AuthenticateUserByName request)
-        {
-            var user = _userManager.Users.FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
+            var user = _userManager.GetUserById(request.Id);
 
             if (user == null)
             {
-                throw new ArgumentException(string.Format("User {0} not found.", request.Username));
+                throw new ResourceNotFoundException("User not found");
             }
 
-            var result = AuthenticateUser(new AuthenticateUser { Id = user.Id, Password = request.Password }).Result;
-
-            return ToOptimizedResult(result);
+            return Post(new AuthenticateUserByName
+            {
+                Username = user.Name,
+                Password = request.Password
+            });
         }
 
-        private async Task<AuthenticationResult> AuthenticateUser(AuthenticateUser request)
+        public async Task<object> Post(AuthenticateUserByName request)
         {
-            var user = _userManager.GetUserById(request.Id);
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
 
-            if (user == null)
+            if (string.IsNullOrWhiteSpace(auth.Client))
             {
-                throw new ResourceNotFoundException("User not found");
+                auth.Client = "Unknown app";
             }
-
-            var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
-
-            // Login in the old way if the header is missing
-            if (string.IsNullOrEmpty(auth.Client) ||
-                string.IsNullOrEmpty(auth.Device) ||
-                string.IsNullOrEmpty(auth.DeviceId) ||
-                string.IsNullOrEmpty(auth.Version))
+            if (string.IsNullOrWhiteSpace(auth.Device))
             {
-                var success = await _userManager.AuthenticateUser(user, request.Password).ConfigureAwait(false);
-
-                if (!success)
-                {
-                    // Unauthorized
-                    throw new UnauthorizedAccessException("Invalid user or password entered.");
-                }
-
-                return new AuthenticationResult
-                {
-                    User = _dtoService.GetUserDto(user)
-                };
+                auth.Device = "Unknown device";
+            }
+            if (string.IsNullOrWhiteSpace(auth.Version))
+            {
+                auth.Version = "Unknown version";
+            }
+            if (string.IsNullOrWhiteSpace(auth.DeviceId))
+            {
+                auth.DeviceId = "Unknown device id";
             }
 
-            var session = await _sessionMananger.AuthenticateNewSession(user, request.Password, auth.Client, auth.Version,
-                        auth.DeviceId, auth.Device, Request.RemoteIp).ConfigureAwait(false);
-
-            var result = new AuthenticationResult
+            var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
             {
-                User = _dtoService.GetUserDto(user),
-                SessionInfo = _sessionMananger.GetSessionInfoDto(session)
-            };
+                App = auth.Client,
+                AppVersion = auth.Version,
+                DeviceId = auth.DeviceId,
+                DeviceName = auth.Device,
+                Password = request.Password,
+                RemoteEndPoint = Request.RemoteIp,
+                Username = request.Username
+
+            }, Request.IsLocal).ConfigureAwait(false);
 
-            return result;
+            return ToOptimizedResult(result);
         }
 
         /// <summary>
@@ -334,6 +355,12 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="request">The request.</param>
         public void Post(UpdateUserPassword request)
+        {
+            var task = PostAsync(request);
+            Task.WaitAll(task);
+        }
+
+        public async Task PostAsync(UpdateUserPassword request)
         {
             var user = _userManager.GetUserById(request.Id);
 
@@ -344,30 +371,33 @@ namespace MediaBrowser.Api
 
             if (request.ResetPassword)
             {
-                var task = _userManager.ResetPassword(user);
-
-                Task.WaitAll(task);
+                await _userManager.ResetPassword(user).ConfigureAwait(false);
             }
             else
             {
-                var success = _userManager.AuthenticateUser(user, request.CurrentPassword).Result;
+                var success = await _userManager.AuthenticateUser(user.Name, request.CurrentPassword, Request.RemoteIp).ConfigureAwait(false);
 
                 if (!success)
                 {
-                    throw new UnauthorizedAccessException("Invalid user or password entered.");
+                    throw new ArgumentException("Invalid user or password entered.");
                 }
 
-                var task = _userManager.ChangePassword(user, request.NewPassword);
-
-                Task.WaitAll(task);
+                await _userManager.ChangePassword(user, request.NewPassword).ConfigureAwait(false);
             }
         }
-
+        
         /// <summary>
         /// Posts the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
         public void Post(UpdateUser request)
+        {
+            var task = PostAsync(request);
+
+            Task.WaitAll(task);
+        }
+
+        public async Task PostAsync(UpdateUser request)
         {
             // 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
@@ -400,11 +430,15 @@ namespace MediaBrowser.Api
                 {
                     throw new ArgumentException("There must be at least one enabled user in the system.");
                 }
+
+                await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false);
             }
 
-            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? _userManager.UpdateUser(user) : _userManager.RenameUser(user, dtoUser.Name);
+            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ?
+                _userManager.UpdateUser(user) :
+                _userManager.RenameUser(user, dtoUser.Name);
 
-            Task.WaitAll(task);
+            await task.ConfigureAwait(false);
 
             user.UpdateConfiguration(dtoUser.Configuration);
         }
@@ -422,7 +456,7 @@ namespace MediaBrowser.Api
 
             newUser.UpdateConfiguration(dtoUser.Configuration);
 
-            var result = _dtoService.GetUserDto(newUser);
+            var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
 
             return ToOptimizedResult(result);
         }

+ 10 - 8
MediaBrowser.Api/VideosService.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
@@ -41,6 +42,7 @@ namespace MediaBrowser.Api
         public string Ids { get; set; }
     }
 
+    [Authenticated]
     public class VideosService : BaseApiService
     {
         private readonly ILibraryManager _libraryManager;
@@ -97,12 +99,12 @@ namespace MediaBrowser.Api
 
         public void Delete(DeleteAlternateSources request)
         {
-            var task = RemoveAlternateVersions(request);
+            var task = DeleteAsync(request);
 
             Task.WaitAll(task);
         }
 
-        private async Task RemoveAlternateVersions(DeleteAlternateSources request)
+        public async Task DeleteAsync(DeleteAlternateSources request)
         {
             var video = (Video)_libraryManager.GetItemById(request.Id);
 
@@ -119,12 +121,12 @@ namespace MediaBrowser.Api
 
         public void Post(MergeVersions request)
         {
-            var task = MergeVersions(request);
+            var task = PostAsync(request);
 
             Task.WaitAll(task);
         }
 
-        private async Task MergeVersions(MergeVersions request)
+        public async Task PostAsync(MergeVersions request)
         {
             var items = request.Ids.Split(',')
                 .Select(i => new Guid(i))
@@ -170,12 +172,12 @@ namespace MediaBrowser.Api
                     return 0;
                 })
                     .ThenByDescending(i =>
-                {
-                    var stream = i.GetDefaultVideoStream();
+                    {
+                        var stream = i.GetDefaultVideoStream();
 
-                    return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+                        return stream == null || stream.Width == null ? 0 : stream.Width.Value;
 
-                }).First();
+                    }).First();
             }
 
             foreach (var item in videos.Where(i => i.Id != primaryVersion.Id))

+ 0 - 149
MediaBrowser.Api/WebSocket/LogFileWebSocketListener.cs

@@ -1,149 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Model.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.WebSocket
-{
-    /// <summary>
-    /// Class ScheduledTasksWebSocketListener
-    /// </summary>
-    public class LogFileWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<string>, LogFileWebSocketState>
-    {
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name
-        {
-            get { return "LogFile"; }
-        }
-
-        /// <summary>
-        /// The _kernel
-        /// </summary>
-        private readonly ILogManager _logManager;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LogFileWebSocketListener" /> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="logManager">The log manager.</param>
-        public LogFileWebSocketListener(ILogger logger, ILogManager logManager, IFileSystem fileSystem)
-            : base(logger)
-        {
-            _logManager = logManager;
-            _fileSystem = fileSystem;
-            _logManager.LoggerLoaded += kernel_LoggerLoaded;
-        }
-
-        /// <summary>
-        /// Gets the data to send.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>IEnumerable{System.String}.</returns>
-        protected override async Task<IEnumerable<string>> GetDataToSend(LogFileWebSocketState state)
-        {
-            if (!string.Equals(_logManager.LogFilePath, state.LastLogFilePath))
-            {
-                state.LastLogFilePath = _logManager.LogFilePath;
-                state.StartLine = 0;
-            }
-
-            var lines = await GetLogLines(state.LastLogFilePath, state.StartLine, _fileSystem).ConfigureAwait(false);
-
-            state.StartLine += lines.Count;
-
-            return lines;
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected override void Dispose(bool dispose)
-        {
-            if (dispose)
-            {
-                _logManager.LoggerLoaded -= kernel_LoggerLoaded;
-            }
-            base.Dispose(dispose);
-        }
-
-        /// <summary>
-        /// Handles the LoggerLoaded event of the kernel control.
-        /// </summary>
-        /// <param name="sender">The source of the event.</param>
-        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
-        void kernel_LoggerLoaded(object sender, EventArgs e)
-        {
-            // Reset the startline for each connection whenever the logger reloads
-            lock (ActiveConnections)
-            {
-                foreach (var connection in ActiveConnections)
-                {
-                    connection.Item4.StartLine = 0;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the log lines.
-        /// </summary>
-        /// <param name="logFilePath">The log file path.</param>
-        /// <param name="startLine">The start line.</param>
-        /// <returns>Task{IEnumerable{System.String}}.</returns>
-        internal static async Task<List<string>> GetLogLines(string logFilePath, int startLine, IFileSystem fileSystem)
-        {
-            var lines = new List<string>();
-
-            using (var fs = fileSystem.GetFileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-            {
-                using (var reader = new StreamReader(fs))
-                {
-                    while (!reader.EndOfStream)
-                    {
-                        var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
-                        if (line.IndexOf("Info -", StringComparison.OrdinalIgnoreCase) != -1 ||
-                            line.IndexOf("Warn -", StringComparison.OrdinalIgnoreCase) != -1 ||
-                            line.IndexOf("Error -", StringComparison.OrdinalIgnoreCase) != -1)
-                        {
-                            lines.Add(line);
-                        }
-                    }
-                }
-            }
-
-            if (startLine > 0)
-            {
-                lines = lines.Skip(startLine).ToList();
-            }
-
-            return lines;
-        }
-    }
-
-    /// <summary>
-    /// Class LogFileWebSocketState
-    /// </summary>
-    public class LogFileWebSocketState : WebSocketListenerState
-    {
-        /// <summary>
-        /// Gets or sets the last log file path.
-        /// </summary>
-        /// <value>The last log file path.</value>
-        public string LastLogFilePath { get; set; }
-        /// <summary>
-        /// Gets or sets the start line.
-        /// </summary>
-        /// <value>The start line.</value>
-        public int StartLine { get; set; }
-    }
-}

+ 48 - 8
MediaBrowser.Common.Implementations/BaseApplicationHost.cs

@@ -211,6 +211,8 @@ namespace MediaBrowser.Common.Implementations
             JsonSerializer = CreateJsonSerializer();
 
             Logger = LogManager.GetLogger("App");
+            OnLoggerLoaded(true);
+            LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false);
 
             IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted;
             progress.Report(2);
@@ -219,16 +221,11 @@ namespace MediaBrowser.Common.Implementations
                                          ? LogSeverity.Debug
                                          : LogSeverity.Info;
 
-            // Put the app config in the log for troubleshooting purposes
-            Logger.LogMultiline("Application Configuration:", LogSeverity.Info, new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)));
-
             progress.Report(3);
 
             DiscoverTypes();
             progress.Report(14);
 
-            Logger.Info("Version {0} initializing", ApplicationVersion);
-
             SetHttpLimit();
             progress.Report(15);
 
@@ -245,6 +242,47 @@ namespace MediaBrowser.Common.Implementations
             progress.Report(100);
         }
 
+        protected virtual void OnLoggerLoaded(bool isFirstLoad)
+        {
+            Logger.Info("Application version: {0}", ApplicationVersion);
+
+            if (!isFirstLoad)
+            {
+                LogEnvironmentInfo(Logger, ApplicationPaths);
+            }
+
+            // Put the app config in the log for troubleshooting purposes
+            Logger.LogMultiline("Application configuration:", LogSeverity.Info, new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)));
+
+            if (Plugins != null)
+            {
+                var pluginBuilder = new StringBuilder();
+
+                foreach (var plugin in Plugins)
+                {
+                    pluginBuilder.AppendLine(string.Format("{0} {1}", plugin.Name, plugin.Version));
+                }
+
+                Logger.LogMultiline("Plugins:", LogSeverity.Info, pluginBuilder);
+            }
+        }
+
+        public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
+        {
+            logger.Info("Command line: {0}", string.Join(" ", Environment.GetCommandLineArgs()));
+
+            logger.Info("Server: {0}", Environment.MachineName);
+            logger.Info("Operating system: {0}", Environment.OSVersion.ToString());
+            logger.Info("Processor count: {0}", Environment.ProcessorCount);
+            logger.Info("64-Bit OS: {0}", Environment.Is64BitOperatingSystem);
+            logger.Info("64-Bit Process: {0}", Environment.Is64BitProcess);
+            logger.Info("Program data path: {0}", appPaths.ProgramDataPath);
+
+            logger.Info("Application Path: {0}", appPaths.ApplicationPath);
+
+            logger.Info("*** When reporting issues please include the entire log file. ***".ToUpper());
+        }
+
         protected virtual IJsonSerializer CreateJsonSerializer()
         {
             return new JsonSerializer();
@@ -342,6 +380,7 @@ namespace MediaBrowser.Common.Implementations
         /// </summary>
         protected virtual void FindParts()
         {
+            ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             Plugins = GetExports<IPlugin>();
         }
 
@@ -393,7 +432,7 @@ namespace MediaBrowser.Common.Implementations
                 HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, Logger, FileSystemManager, ConfigurationManager);
                 RegisterSingleInstance(HttpClient);
 
-                NetworkManager = CreateNetworkManager();
+                NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager"));
                 RegisterSingleInstance(NetworkManager);
 
                 SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, NetworkManager, LogManager);
@@ -461,7 +500,7 @@ namespace MediaBrowser.Common.Implementations
             }
         }
 
-        protected abstract INetworkManager CreateNetworkManager();
+        protected abstract INetworkManager CreateNetworkManager(ILogger logger);
 
         /// <summary>
         /// Creates an instance of type and resolves all constructor dependancies
@@ -631,6 +670,7 @@ namespace MediaBrowser.Common.Implementations
             return parts;
         }
 
+        private Version _version;
         /// <summary>
         /// Gets the current application version
         /// </summary>
@@ -639,7 +679,7 @@ namespace MediaBrowser.Common.Implementations
         {
             get
             {
-                return GetType().Assembly.GetName().Version;
+                return _version ?? (_version = GetType().Assembly.GetName().Version);
             }
         }
 

+ 94 - 9
MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs

@@ -1,10 +1,13 @@
-using System.IO;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
 using System.Threading;
 
 namespace MediaBrowser.Common.Implementations.Configuration
@@ -25,6 +28,16 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// </summary>
         public event EventHandler<EventArgs> ConfigurationUpdated;
 
+        /// <summary>
+        /// Occurs when [configuration updating].
+        /// </summary>
+        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+        
+        /// <summary>
+        /// Occurs when [named configuration updated].
+        /// </summary>
+        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+
         /// <summary>
         /// Gets the logger.
         /// </summary>
@@ -74,6 +87,9 @@ namespace MediaBrowser.Common.Implementations.Configuration
             }
         }
 
+        private ConfigurationStore[] _configurationStores = {};
+        private IConfigurationFactory[] _configurationFactories;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
         /// </summary>
@@ -89,10 +105,14 @@ namespace MediaBrowser.Common.Implementations.Configuration
             UpdateCachePath();
         }
 
-        /// <summary>
-        /// The _save lock
-        /// </summary>
-        private readonly object _configurationSaveLock = new object();
+        public void AddParts(IEnumerable<IConfigurationFactory> factories)
+        {
+            _configurationFactories = factories.ToArray();
+
+            _configurationStores = _configurationFactories
+                .SelectMany(i => i.GetConfigurations())
+                .ToArray();
+        }
 
         /// <summary>
         /// Saves the configuration.
@@ -103,7 +123,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
-            lock (_configurationSaveLock)
+            lock (_configurationSyncLock)
             {
                 XmlSerializer.SerializeToFile(CommonConfiguration, path);
             }
@@ -144,8 +164,8 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// </summary>
         private void UpdateCachePath()
         {
-            ((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ? 
-                null : 
+            ((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ?
+                null :
                 CommonConfiguration.CachePath;
         }
 
@@ -168,5 +188,70 @@ namespace MediaBrowser.Common.Implementations.Configuration
                 }
             }
         }
+
+        private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
+
+        private string GetConfigurationFile(string key)
+        {
+            return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLower() + ".xml");
+        }
+
+        public object GetConfiguration(string key)
+        {
+            return _configurations.GetOrAdd(key, k =>
+            {
+                var file = GetConfigurationFile(key);
+
+                var configurationType = _configurationStores
+                    .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase))
+                    .ConfigurationType;
+
+                lock (_configurationSyncLock)
+                {
+                    return ConfigurationHelper.GetXmlConfiguration(configurationType, file, XmlSerializer);
+                }
+            });
+        }
+
+        public void SaveConfiguration(string key, object configuration)
+        {
+            var configurationType = GetConfigurationType(key);
+
+            if (configuration.GetType() != configurationType)
+            {
+                throw new ArgumentException("Expected configuration type is " + configurationType.Name);
+            }
+
+            EventHelper.FireEventIfNotNull(NamedConfigurationUpdating, this, new ConfigurationUpdateEventArgs
+            {
+                Key = key,
+                NewConfiguration = configuration
+
+            }, Logger);
+            
+            _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
+
+            var path = GetConfigurationFile(key);
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+            lock (_configurationSyncLock)
+            {
+                XmlSerializer.SerializeToFile(configuration, path);
+            }
+
+            EventHelper.FireEventIfNotNull(NamedConfigurationUpdated, this, new ConfigurationUpdateEventArgs
+            {
+                Key = key,
+                NewConfiguration = configuration
+
+            }, Logger);
+        }
+
+        public Type GetConfigurationType(string key)
+        {
+            return _configurationStores
+                .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase))
+                .ConfigurationType;
+        }
     }
 }

+ 5 - 17
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -107,7 +107,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             return client;
         }
 
-        private PropertyInfo _httpBehaviorPropertyInfo;
         private WebRequest GetRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
         {
             var request = (HttpWebRequest)WebRequest.Create(options.Url);
@@ -118,7 +117,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
             request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
 
-            request.KeepAlive = options.EnableKeepAlive;
+            if (options.EnableKeepAlive)
+            {
+                request.KeepAlive = true;
+            }
+
             request.Method = method;
             request.Pipelined = true;
             request.Timeout = 20000;
@@ -133,21 +136,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 request.Referer = options.Referer;
             }
 
-#if !__MonoCS__
-            if (options.EnableKeepAlive)
-            {
-                // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
-                // May need to remove this for mono
-                var sp = request.ServicePoint;
-                if (_httpBehaviorPropertyInfo == null)
-                {
-                    _httpBehaviorPropertyInfo = sp.GetType().GetProperty("HttpBehaviour", BindingFlags.Instance | BindingFlags.NonPublic);
-                }
-
-                _httpBehaviorPropertyInfo.SetValue(sp, (byte)0, null);
-            }
-#endif
-
             return request;
         }
 

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

@@ -367,5 +367,20 @@ namespace MediaBrowser.Common.Implementations.IO
 
             return newPath;
         }
+
+        public string GetFileNameWithoutExtension(FileSystemInfo info)
+        {
+            if (info is DirectoryInfo)
+            {
+                return info.Name;
+            }
+
+            return Path.GetFileNameWithoutExtension(info.FullName);
+        }
+
+        public string GetFileNameWithoutExtension(string path)
+        {
+            return Path.GetFileNameWithoutExtension(path);
+        }
     }
 }

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

@@ -48,17 +48,17 @@
     <RunPostBuildEvent>Always</RunPostBuildEvent>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="NLog, Version=3.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
+    <Reference Include="NLog, Version=3.1.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\NLog.3.0.0.0\lib\net45\NLog.dll</HintPath>
+      <HintPath>..\packages\NLog.3.1.0.0\lib\net45\NLog.dll</HintPath>
     </Reference>
     <Reference Include="SimpleInjector, Version=2.5.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\SimpleInjector.2.5.0\lib\net45\SimpleInjector.dll</HintPath>
+      <HintPath>..\packages\SimpleInjector.2.5.2\lib\net45\SimpleInjector.dll</HintPath>
     </Reference>
-    <Reference Include="SimpleInjector.Diagnostics, Version=2.5.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
+    <Reference Include="SimpleInjector.Diagnostics, Version=2.5.2.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\SimpleInjector.2.5.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
+      <HintPath>..\packages\SimpleInjector.2.5.2\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Configuration" />
@@ -122,7 +122,7 @@
   </ItemGroup>
   <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
-  <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
+  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
   <PropertyGroup>
     <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' (
 xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
@@ -135,4 +135,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 85 - 2
MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs

@@ -1,4 +1,5 @@
-using System;
+using MediaBrowser.Model.Logging;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
@@ -10,6 +11,13 @@ namespace MediaBrowser.Common.Implementations.Networking
 {
     public abstract class BaseNetworkManager
     {
+        protected ILogger Logger { get; private set; }
+
+        protected BaseNetworkManager(ILogger logger)
+        {
+            Logger = logger;
+        }
+
         /// <summary>
         /// Gets the machine's local ip address
         /// </summary>
@@ -26,6 +34,81 @@ namespace MediaBrowser.Common.Implementations.Networking
             return GetLocalIpAddressesFallback();
         }
 
+        public bool IsInLocalNetwork(string endpoint)
+        {
+            return IsInLocalNetworkInternal(endpoint, true);
+        }
+
+        public bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
+        {
+            if (string.IsNullOrWhiteSpace(endpoint))
+            {
+                throw new ArgumentNullException("endpoint");
+            }
+
+            const int lengthMatch = 4;
+
+            if (endpoint.Length >= lengthMatch)
+            {
+                var prefix = endpoint.Substring(0, lengthMatch);
+
+                if (GetLocalIpAddresses()
+                    .Any(i => i.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
+                {
+                    return true;
+                }
+            }
+
+            // Private address space:
+            // http://en.wikipedia.org/wiki/Private_network
+
+            var isPrivate =
+
+                // If url was requested with computer name, we may see this
+                endpoint.IndexOf("::", StringComparison.OrdinalIgnoreCase) != -1 ||
+
+                endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
+                endpoint.StartsWith("192.", StringComparison.OrdinalIgnoreCase) ||
+                endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase) ||
+                endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
+
+            if (isPrivate)
+            {
+                return true;
+            }
+
+            IPAddress address;
+            if (resolveHost && !IPAddress.TryParse(endpoint, out address))
+            {
+                var host = new Uri(endpoint).DnsSafeHost;
+
+                Logger.Debug("Resolving host {0}", host);
+
+                try
+                {
+                    address = GetIpAddresses(host).FirstOrDefault();
+
+                    if (address != null)
+                    {
+                        Logger.Debug("{0} resolved to {1}", host, address);
+
+                        return IsInLocalNetworkInternal(address.ToString(), false);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error resovling hostname {0}", ex, host);
+                }
+            }
+
+            return false;
+        }
+        
+        public IEnumerable<IPAddress> GetIpAddresses(string hostName)
+        {
+            return Dns.GetHostAddresses(hostName);
+        }
+
         private IEnumerable<IPAddress> GetIPsDefault()
         {
             foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
@@ -63,7 +146,7 @@ namespace MediaBrowser.Common.Implementations.Networking
                 .Select(i => i.ToString())
                 .Reverse();
         }
-        
+
         /// <summary>
         /// Gets a random port number that is currently available
         /// </summary>

+ 1 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -547,6 +547,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             if (ex != null)
             {
                 result.ErrorMessage = ex.Message;
+                result.LongErrorMessage = ex.StackTrace;
             }
 
             var path = GetHistoryFilePath();

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

@@ -46,7 +46,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             return new ITaskTrigger[] { 
             
                 // At startup
-                new StartupTrigger (),
+                new StartupTrigger {DelayMs = 60000},
 
                 // Every so often
                 new IntervalTrigger { Interval = TimeSpan.FromHours(24)}

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

@@ -43,7 +43,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             return new ITaskTrigger[] { 
             
                 // At startup
-                new StartupTrigger (),
+                new StartupTrigger {DelayMs = 30000},
 
                 // Every so often
                 new IntervalTrigger { Interval = TimeSpan.FromHours(24)}

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

@@ -41,7 +41,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             return new ITaskTrigger[] { 
             
                 // At startup
-                new StartupTrigger (),
+                new StartupTrigger(),
 
                 // Every so often
                 new IntervalTrigger { Interval = TimeSpan.FromHours(24)}

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

@@ -52,7 +52,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
             return new ITaskTrigger[] { 
             
                 // At startup
-                new StartupTrigger (),
+                new StartupTrigger(),
 
                 // Every so often
                 new IntervalTrigger { Interval = TimeSpan.FromHours(24)}

+ 1 - 4
MediaBrowser.Common.Implementations/Security/UsageReporter.cs

@@ -26,16 +26,13 @@ namespace MediaBrowser.Common.Implementations.Security
 
             var mac = _networkManager.GetMacAddress();
 
-            var plugins = string.Join("|", _applicationHost.Plugins.Select(i => i.Name).ToArray());
-
             var data = new Dictionary<string, string>
             {
                 { "feature", _applicationHost.Name }, 
                 { "mac", mac }, 
                 { "ver", _applicationHost.ApplicationVersion.ToString() }, 
                 { "platform", Environment.OSVersion.VersionString }, 
-                { "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}, 
-                { "plugins", plugins}
+                { "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}
             };
 
             return _httpClient.Post(Constants.Constants.MbAdminUrl + "service/registration/ping", data, cancellationToken);

+ 0 - 20
MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs

@@ -210,25 +210,5 @@ namespace MediaBrowser.Common.Implementations.Serialization
 
             return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType());
         }
-
-        /// <summary>
-        /// Serializes to bytes.
-        /// </summary>
-        /// <param name="obj">The obj.</param>
-        /// <returns>System.Byte[][].</returns>
-        /// <exception cref="System.ArgumentNullException">obj</exception>
-        public byte[] SerializeToBytes(object obj)
-        {
-            if (obj == null)
-            {
-                throw new ArgumentNullException("obj");
-            }
-
-            using (var stream = new MemoryStream())
-            {
-                SerializeToStream(obj, stream);
-                return stream.ToArray();
-            }
-        }
     }
 }

+ 0 - 15
MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs

@@ -91,20 +91,5 @@ namespace MediaBrowser.Common.Implementations.Serialization
                 return DeserializeFromStream(type, stream);
             }
         }
-
-        /// <summary>
-        /// Serializes to bytes.
-        /// </summary>
-        /// <param name="obj">The obj.</param>
-        /// <returns>System.Byte[][].</returns>
-        public byte[] SerializeToBytes(object obj)
-        {
-            using (var stream = new MemoryStream())
-            {
-                SerializeToStream(obj, stream);
-
-                return stream.ToArray();
-            }
-        }
     }
 }

+ 34 - 14
MediaBrowser.Common.Implementations/Updates/InstallationManager.cs

@@ -68,7 +68,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         /// <param name="newVersion">The new version.</param>
         private void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion)
         {
-            _logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.version, newVersion.classification);
+            _logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.versionStr ?? string.Empty, newVersion.classification);
 
             EventHelper.FireEventIfNotNull(PluginUpdated, this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) }, _logger);
 
@@ -87,7 +87,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         /// <param name="package">The package.</param>
         private void OnPluginInstalled(PackageVersionInfo package)
         {
-            _logger.Info("New plugin installed: {0} {1} {2}", package.name, package.version, package.classification);
+            _logger.Info("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
 
             EventHelper.FireEventIfNotNull(PluginInstalled, this, new GenericEventArgs<PackageVersionInfo> { Argument = package }, _logger);
 
@@ -133,6 +133,16 @@ namespace MediaBrowser.Common.Implementations.Updates
             _logger = logger;
         }
 
+        private Version GetPackageVersion(PackageVersionInfo version)
+        {
+            return new Version(ValueOrDefault(version.versionStr, "0.0.0.1"));
+        }
+
+        private static string ValueOrDefault(string str, string def)
+        {
+            return string.IsNullOrEmpty(str) ? def : str;
+        }
+
         /// <summary>
         /// Gets all available packages.
         /// </summary>
@@ -167,17 +177,27 @@ namespace MediaBrowser.Common.Implementations.Updates
         {
             if (_lastPackageListResult != null)
             {
-                // Let dev users get results more often for testing purposes
-                var cacheLength = _config.CommonConfiguration.SystemUpdateLevel == PackageVersionClass.Dev
-                                      ? TimeSpan.FromMinutes(3)
-                                      : TimeSpan.FromHours(6);
+                TimeSpan cacheLength;
+
+                switch (_config.CommonConfiguration.SystemUpdateLevel)
+                {
+                    case PackageVersionClass.Beta:
+                        cacheLength = TimeSpan.FromMinutes(30);
+                        break;
+                    case PackageVersionClass.Dev:
+                        cacheLength = TimeSpan.FromMinutes(3);
+                        break;
+                    default:
+                        cacheLength = TimeSpan.FromHours(6);
+                        break;
+                }
 
                 if ((DateTime.UtcNow - _lastPackageListResult.Item2) < cacheLength)
                 {
                     return _lastPackageListResult.Item1;
                 }
             }
-            
+
             using (var json = await _httpClient.Get(Constants.Constants.MbAdminUrl + "service/MB3Packages.json", cancellationToken).ConfigureAwait(false))
             {
                 cancellationToken.ThrowIfCancellationRequested();
@@ -197,7 +217,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             foreach (var package in packages)
             {
                 package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl))
-                    .OrderByDescending(v => v.version).ToList();
+                    .OrderByDescending(GetPackageVersion).ToList();
             }
 
             // Remove packages with no versions
@@ -211,7 +231,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             foreach (var package in packages)
             {
                 package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl))
-                    .OrderByDescending(v => v.version).ToList();
+                    .OrderByDescending(GetPackageVersion).ToList();
             }
 
             if (packageType.HasValue)
@@ -264,7 +284,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         {
             var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false);
 
-            var package = packages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase)) 
+            var package = packages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
                             ?? packages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
 
             if (package == null)
@@ -272,7 +292,7 @@ namespace MediaBrowser.Common.Implementations.Updates
                 return null;
             }
 
-            return package.versions.FirstOrDefault(v => v.version.Equals(version) && v.classification == classification);
+            return package.versions.FirstOrDefault(v => GetPackageVersion(v).Equals(version) && v.classification == classification);
         }
 
         /// <summary>
@@ -300,7 +320,7 @@ namespace MediaBrowser.Common.Implementations.Updates
         /// <returns>PackageVersionInfo.</returns>
         public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release)
         {
-            var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase)) 
+            var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
                             ?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
 
             if (package == null)
@@ -309,7 +329,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             }
 
             return package.versions
-                .OrderByDescending(v => v.version)
+                .OrderByDescending(GetPackageVersion)
                 .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion));
         }
 
@@ -338,7 +358,7 @@ namespace MediaBrowser.Common.Implementations.Updates
             {
                 var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, _config.CommonConfiguration.SystemUpdateLevel);
 
-                return latestPluginInfo != null && latestPluginInfo.version != null && latestPluginInfo.version > p.Version ? latestPluginInfo : null;
+                return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null;
 
             }).Where(i => i != null).ToList();
 

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

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="NLog" version="3.0.0.0" targetFramework="net45" />
+  <package id="NLog" version="3.1.0.0" targetFramework="net45" />
   <package id="sharpcompress" version="0.10.2" targetFramework="net45" />
-  <package id="SimpleInjector" version="2.5.0" targetFramework="net45" />
+  <package id="SimpleInjector" version="2.5.2" targetFramework="net45" />
 </packages>

+ 16 - 12
MediaBrowser.Common/Configuration/ConfigurationHelper.cs

@@ -36,21 +36,25 @@ namespace MediaBrowser.Common.Configuration
                 configuration = Activator.CreateInstance(type);
             }
 
-            // Take the object we just got and serialize it back to bytes
-            var newBytes = xmlSerializer.SerializeToBytes(configuration);
-
-            // If the file didn't exist before, or if something has changed, re-save
-            if (buffer == null || !buffer.SequenceEqual(newBytes))
+            using (var stream = new MemoryStream())
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
-                
-                // Save it after load in case we got new items
-                File.WriteAllBytes(path, newBytes);
-            }
+                xmlSerializer.SerializeToStream(configuration, stream);
 
-            return configuration;
-        }
+                // Take the object we just got and serialize it back to bytes
+                var newBytes = stream.ToArray();
+
+                // If the file didn't exist before, or if something has changed, re-save
+                if (buffer == null || !buffer.SequenceEqual(newBytes))
+                {
+                    Directory.CreateDirectory(Path.GetDirectoryName(path));
 
+                    // Save it after load in case we got new items
+                    File.WriteAllBytes(path, newBytes);
+                }
+
+                return configuration;
+            }
+        }
 
         /// <summary>
         /// Reads an xml configuration file from the file system

+ 18 - 0
MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace MediaBrowser.Common.Configuration
+{
+    public class ConfigurationUpdateEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the key.
+        /// </summary>
+        /// <value>The key.</value>
+        public string Key { get; set; }
+        /// <summary>
+        /// Gets or sets the new configuration.
+        /// </summary>
+        /// <value>The new configuration.</value>
+        public object NewConfiguration { get; set; }
+    }
+}

+ 17 - 0
MediaBrowser.Common/Configuration/IConfigurationFactory.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Configuration
+{
+    public interface IConfigurationFactory
+    {
+        IEnumerable<ConfigurationStore> GetConfigurations();
+    }
+
+    public class ConfigurationStore
+    {
+        public string Key { get; set; }
+
+        public Type ConfigurationType { get; set; }
+    }
+}

+ 47 - 1
MediaBrowser.Common/Configuration/IConfigurationManager.cs

@@ -1,15 +1,26 @@
 using MediaBrowser.Model.Configuration;
 using System;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Common.Configuration
 {
     public interface IConfigurationManager
     {
+        /// <summary>
+        /// Occurs when [configuration updating].
+        /// </summary>
+        event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+
         /// <summary>
         /// Occurs when [configuration updated].
         /// </summary>
         event EventHandler<EventArgs> ConfigurationUpdated;
-        
+
+        /// <summary>
+        /// Occurs when [named configuration updated].
+        /// </summary>
+        event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+
         /// <summary>
         /// Gets or sets the application paths.
         /// </summary>
@@ -32,5 +43,40 @@ namespace MediaBrowser.Common.Configuration
         /// </summary>
         /// <param name="newConfiguration">The new configuration.</param>
         void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns>System.Object.</returns>
+        object GetConfiguration(string key);
+
+        /// <summary>
+        /// Gets the type of the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns>Type.</returns>
+        Type GetConfigurationType(string key);
+
+        /// <summary>
+        /// Saves the configuration.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <param name="configuration">The configuration.</param>
+        void SaveConfiguration(string key, object configuration);
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="factories">The factories.</param>
+        void AddParts(IEnumerable<IConfigurationFactory> factories);
+    }
+
+    public static class ConfigurationManagerExtensions
+    {
+        public static T GetConfiguration<T>(this IConfigurationManager manager, string key)
+        {
+            return (T)manager.GetConfiguration(key);
+        }
     }
 }

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

@@ -112,5 +112,19 @@ namespace MediaBrowser.Common.IO
         /// <param name="to">To.</param>
         /// <returns>System.String.</returns>
         string SubstitutePath(string path, string from, string to);
+
+        /// <summary>
+        /// Gets the file name without extension.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <returns>System.String.</returns>
+        string GetFileNameWithoutExtension(FileSystemInfo info);
+
+        /// <summary>
+        /// Gets the file name without extension.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>System.String.</returns>
+        string GetFileNameWithoutExtension(string path);
     }
 }

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

@@ -55,7 +55,9 @@
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     <Compile Include="Configuration\ConfigurationHelper.cs" />
+    <Compile Include="Configuration\ConfigurationUpdateEventArgs.cs" />
     <Compile Include="Configuration\IConfigurationManager.cs" />
+    <Compile Include="Configuration\IConfigurationFactory.cs" />
     <Compile Include="Constants\Constants.cs" />
     <Compile Include="Events\EventHelper.cs" />
     <Compile Include="Extensions\BaseExtensions.cs" />
@@ -76,7 +78,6 @@
     <Compile Include="Net\INetworkManager.cs" />
     <Compile Include="Net\IWebSocket.cs" />
     <Compile Include="Net\IWebSocketConnection.cs" />
-    <Compile Include="Net\IWebSocketServer.cs" />
     <Compile Include="Net\MimeTypes.cs" />
     <Compile Include="Net\WebSocketConnectEventArgs.cs" />
     <Compile Include="Net\WebSocketMessageInfo.cs" />
@@ -115,7 +116,7 @@
   </ItemGroup>
   <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
-  <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
+  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
   <PropertyGroup>
     <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' (
 xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
@@ -128,4 +129,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

Some files were not shown because too many files changed in this diff