Explorar el Código

#514 - Support HLS seeking

Luke Pulverenti hace 11 años
padre
commit
3bef6ead9c

+ 5 - 4
MediaBrowser.Api/BaseApiService.cs

@@ -159,7 +159,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))
             {
@@ -169,7 +169,7 @@ namespace MediaBrowser.Api
                 {
                     var user = userManager.GetUserById(userId.Value);
 
-                    return folder.GetRecursiveChildren(user).ToList();
+                    return folder.GetRecursiveChildren(user);
                 }
 
                 return folder.GetRecursiveChildren();
@@ -178,7 +178,7 @@ namespace MediaBrowser.Api
             {
                 var user = userManager.GetUserById(userId.Value);
 
-                return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user, null);
+                return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
             }
 
             return libraryManager.RootFolder.GetRecursiveChildren();
@@ -239,7 +239,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 =>

+ 1 - 1
MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs

@@ -381,7 +381,7 @@ namespace MediaBrowser.Api.DefaultTheme
             var currentUser1 = user;
 
             var ownedEpisodes = series
-                .SelectMany(i => i.GetRecursiveChildren(currentUser1, j => j.LocationType != LocationType.Virtual))
+                .SelectMany(i => i.GetRecursiveChildren(currentUser1).Where(j => j.LocationType != LocationType.Virtual))
                 .OfType<Episode>()
                 .ToList();
 

+ 46 - 17
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -59,10 +59,11 @@ 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);
@@ -70,13 +71,12 @@ namespace MediaBrowser.Api.Playback.Hls
         /// 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();
 
@@ -110,7 +110,8 @@ 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
@@ -119,6 +120,22 @@ namespace MediaBrowser.Api.Playback.Hls
                 }
             }
 
+            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);
+                }
+            }
+
             var audioBitrate = state.OutputAudioBitrate ?? 0;
             var videoBitrate = state.OutputVideoBitrate ?? 0;
 
@@ -188,16 +205,18 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
         {
-            var count = 0;
+            Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
 
-            // 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))
+            while (true)
             {
-                using (var reader = new StreamReader(fileStream))
+                // 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))
                 {
-                    while (true)
+                    using (var reader = new StreamReader(fileStream))
                     {
-                        if (!reader.EndOfStream)
+                        var count = 0;
+
+                        while (!reader.EndOfStream)
                         {
                             var line = await reader.ReadLineAsync().ConfigureAwait(false);
 
@@ -206,11 +225,12 @@ namespace MediaBrowser.Api.Playback.Hls
                                 count++;
                                 if (count >= segmentCount)
                                 {
+                                    Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
                                     return;
                                 }
                             }
                         }
-                        await Task.Delay(25, cancellationToken).ConfigureAwait(false);
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
                     }
                 }
             }
@@ -229,7 +249,7 @@ namespace MediaBrowser.Api.Playback.Hls
             
             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));
 
@@ -240,7 +260,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),
@@ -251,6 +279,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 state.SegmentLength.ToString(UsCulture),
                 startNumberParam,
                 state.HlsListSize.ToString(UsCulture),
+                baseUrlParam,
                 outputPath
                 ).Trim();
 

+ 6 - 36
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -22,11 +22,6 @@ namespace MediaBrowser.Api.Playback.Hls
     [Api(Description = "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; }
-
-        [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; }
     }
 
     [Route("/Videos/{Id}/main.m3u8", "GET")]
@@ -35,12 +30,6 @@ namespace MediaBrowser.Api.Playback.Hls
     {
     }
 
-    [Route("/Videos/{Id}/baseline.m3u8", "GET")]
-    [Api(Description = "Gets a video stream using HTTP live streaming.")]
-    public class GetBaselineHlsVideoStream : VideoStreamRequest
-    {
-    }
-
     /// <summary>
     /// Class GetHlsVideoSegment
     /// </summary>
@@ -73,16 +62,11 @@ namespace MediaBrowser.Api.Playback.Hls
 
         public object Get(GetDynamicHlsVideoSegment request)
         {
-            if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
-            {
-                return GetDynamicSegment(request, false).Result;
-            }
-
-            return GetDynamicSegment(request, true).Result;
+            return GetDynamicSegment(request).Result;
         }
 
         private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
-        private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
+        private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request)
         {
             if ((request.StartTimeTicks ?? 0) > 0)
             {
@@ -322,7 +306,9 @@ namespace MediaBrowser.Api.Playback.Hls
             var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
 
             // Main stream
-            var playlistUrl = "main.m3u8" + queryString;
+            var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
+            playlistUrl += queryString;
+
             AppendPlaylist(builder, playlistUrl, totalBitrate);
 
             if (state.VideoRequest.VideoBitRate.HasValue)
@@ -385,13 +371,6 @@ namespace MediaBrowser.Api.Playback.Hls
             return result;
         }
 
-        public object Get(GetBaselineHlsVideoStream request)
-        {
-            var result = GetPlaylistAsync(request, "baseline").Result;
-
-            return result;
-        }
-
         private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
         {
             var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
@@ -506,14 +485,6 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.String.</returns>
         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;
-
-            var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
-
             var threads = GetNumberOfThreads(state, false);
 
             var inputModifier = GetInputModifier(state);
@@ -521,8 +492,7 @@ 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} -flags -global_header {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
-                itsOffset,
+            var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
                 inputModifier,
                 GetInputArgument(state),
                 threads,

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

@@ -32,6 +32,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 +111,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>

+ 2 - 2
MediaBrowser.Api/SearchService.cs

@@ -192,14 +192,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;

+ 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();

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

@@ -541,7 +541,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)

+ 2 - 25
MediaBrowser.Controller/Entities/Folder.cs

@@ -856,19 +856,6 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>IEnumerable{BaseItem}.</returns>
         /// <exception cref="System.ArgumentNullException"></exception>
         public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
-        {
-            return GetRecursiveChildren(user, null, includeLinkedChildren);
-        }
-
-        /// <summary>
-        /// Gets the recursive children.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="filter">The filter.</param>
-        /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
-        /// <returns>IList{BaseItem}.</returns>
-        /// <exception cref="System.ArgumentNullException"></exception>
-        public IList<BaseItem> GetRecursiveChildren(User user, Func<BaseItem, bool> filter, bool includeLinkedChildren = true)
         {
             if (user == null)
             {
@@ -877,7 +864,7 @@ namespace MediaBrowser.Controller.Entities
 
             var list = new List<BaseItem>();
 
-            var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, true, filter);
+            var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, true, null);
 
             return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
         }
@@ -887,20 +874,10 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <returns>IList{BaseItem}.</returns>
         public IList<BaseItem> GetRecursiveChildren()
-        {
-            return GetRecursiveChildren(i => true);
-        }
-
-        /// <summary>
-        /// Gets the recursive children.
-        /// </summary>
-        /// <param name="filter">The filter.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
         {
             var list = new List<BaseItem>();
 
-            AddChildrenToList(list, true, filter);
+            AddChildrenToList(list, true, null);
 
             return list;
         }

+ 9 - 3
MediaBrowser.Controller/Entities/UserView.cs

@@ -18,11 +18,17 @@ namespace MediaBrowser.Controller.Entities
             switch (ViewType)
             {
                 case CollectionType.Games:
-                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<GameSystem>();
+                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
+                        .OfType<GameSystem>();
                 case CollectionType.BoxSets:
-                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<BoxSet>();
+                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
+                        .OfType<BoxSet>();
                 case CollectionType.TvShows:
-                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<Series>();
+                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
+                        .OfType<Series>();
+                case CollectionType.Trailers:
+                    return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
+                        .OfType<Trailer>();
                 default:
                     return mediaFolders.SelectMany(i => i.GetChildren(user, includeLinkedChildren));
             }

+ 7 - 0
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -217,6 +217,7 @@ namespace MediaBrowser.Model.ApiClient
         /// Gets the additional parts.
         /// </summary>
         /// <param name="itemId">The item identifier.</param>
+        /// <param name="userId">The user identifier.</param>
         /// <returns>Task{BaseItemDto[]}.</returns>
         Task<ItemsResult> GetAdditionalParts(string itemId, string userId);
         
@@ -241,6 +242,12 @@ namespace MediaBrowser.Model.ApiClient
         /// <returns>Task{SessionInfoDto[]}.</returns>
         Task<SessionInfoDto[]> GetClientSessionsAsync(SessionQuery query);
 
+        /// <summary>
+        /// Gets the client session asynchronous.
+        /// </summary>
+        /// <returns>Task{SessionInfoDto}.</returns>
+        Task<SessionInfoDto> GetCurrentSessionAsync();
+        
         /// <summary>
         /// Gets the item counts async.
         /// </summary>

+ 0 - 2
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -93,8 +93,6 @@ namespace MediaBrowser.Model.Configuration
             BlockUnratedItems = new UnratedItem[] { };
 
             ExcludeFoldersFromGrouping = new string[] { };
-
-            DisplayCollectionsView = true;
         }
     }
 }

+ 7 - 2
MediaBrowser.Model/Dto/StudioDto.cs

@@ -1,5 +1,4 @@
-using System;
-using System.ComponentModel;
+using System.ComponentModel;
 using System.Diagnostics;
 using System.Runtime.Serialization;
 
@@ -17,6 +16,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The name.</value>
         public string Name { get; set; }
 
+        /// <summary>
+        /// Gets or sets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        public string Id { get; set; }
+        
         /// <summary>
         /// Gets or sets the primary image tag.
         /// </summary>

+ 5 - 2
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -328,7 +328,8 @@ namespace MediaBrowser.Server.Implementations.Dto
             if (!string.IsNullOrEmpty(item.Album))
             {
                 var parentAlbum = _libraryManager.RootFolder
-                    .GetRecursiveChildren(i => i is MusicAlbum)
+                    .GetRecursiveChildren()
+                    .Where(i => i is MusicAlbum)
                     .FirstOrDefault(i => string.Equals(i.Name, item.Album, StringComparison.OrdinalIgnoreCase));
 
                 if (parentAlbum != null)
@@ -539,6 +540,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
                 if (dictionary.TryGetValue(studio, out entity))
                 {
+                    studioDto.Id = entity.Id.ToString("N");
                     studioDto.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
                 }
 
@@ -1248,7 +1250,8 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
             else
             {
-                children = folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual);
+                children = folder.GetRecursiveChildren(user)
+                    .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual);
             }
 
             // Loop through each recursive child

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

@@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 var user = _userManager.GetUserById(new Guid(query.UserId));
 
-                inputItems = user.RootFolder.GetRecursiveChildren(user, null);
+                inputItems = user.RootFolder.GetRecursiveChildren(user, true);
             }
 
 

+ 3 - 2
MediaBrowser.Server.Implementations/Library/UserViewManager.cs

@@ -15,6 +15,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Server.Implementations.Library
 {
@@ -84,7 +85,7 @@ namespace MediaBrowser.Server.Implementations.Library
             if (user.Configuration.DisplayCollectionsView ||
                 recursiveChildren.OfType<BoxSet>().Any())
             {
-                list.Add(await GetUserView(CollectionType.BoxSets, user, "zzz_" + CollectionType.BoxSets, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(CollectionType.BoxSets, user, CollectionType.BoxSets, cancellationToken).ConfigureAwait(false));
             }
 
             if (query.IncludeExternalContent)
@@ -114,7 +115,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 }
             }
 
-            return list.OrderBy(i => i.SortName);
+            return _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).Cast<Folder>();
         }
 
         private Task<UserView> GetUserView(string type, User user, string sortName, CancellationToken cancellationToken)