Browse Source

Merge pull request #848 from Bond-009/perf

Minor changes to reduce allocations
Joshua M. Boniface 6 years ago
parent
commit
89d4ce309d

+ 2 - 25
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -224,7 +224,7 @@ namespace Emby.Server.Implementations.Data
                 });
             }
 
-            db.ExecuteAll(string.Join(";", queries.ToArray()));
+            db.ExecuteAll(string.Join(";", queries));
             Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
         }
 
@@ -232,23 +232,6 @@ namespace Emby.Server.Implementations.Data
 
         protected virtual int? CacheSize => null;
 
-        internal static void CheckOk(int rc)
-        {
-            string msg = "";
-
-            if (raw.SQLITE_OK != rc)
-            {
-                throw CreateException((ErrorCode)rc, msg);
-            }
-        }
-
-        internal static Exception CreateException(ErrorCode rc, string msg)
-        {
-            var exp = new Exception(msg);
-
-            return exp;
-        }
-
         private bool _disposed;
         protected void CheckDisposed()
         {
@@ -375,13 +358,6 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public class DummyToken : IDisposable
-        {
-            public void Dispose()
-            {
-            }
-        }
-
         public static IDisposable Read(this ReaderWriterLockSlim obj)
         {
             //if (BaseSqliteRepository.ThreadSafeMode > 0)
@@ -390,6 +366,7 @@ namespace Emby.Server.Implementations.Data
             //}
             return new WriteLockToken(obj);
         }
+
         public static IDisposable Write(this ReaderWriterLockSlim obj)
         {
             //if (BaseSqliteRepository.ThreadSafeMode > 0)

+ 139 - 146
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -536,7 +536,7 @@ namespace Emby.Server.Implementations.Data
                 throw new ArgumentNullException(nameof(item));
             }
 
-            SaveItems(new List<BaseItem> { item }, cancellationToken);
+            SaveItems(new [] { item }, cancellationToken);
         }
 
         public void SaveImages(BaseItem item)
@@ -576,7 +576,7 @@ namespace Emby.Server.Implementations.Data
         /// or
         /// cancellationToken
         /// </exception>
-        public void SaveItems(List<BaseItem> items, CancellationToken cancellationToken)
+        public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
         {
             if (items == null)
             {
@@ -587,7 +587,7 @@ namespace Emby.Server.Implementations.Data
 
             CheckDisposed();
 
-            var tuples = new List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>>();
+            var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>();
             foreach (var item in items)
             {
                 var ancestorIds = item.SupportsAncestors ?
@@ -599,7 +599,7 @@ namespace Emby.Server.Implementations.Data
                 var userdataKey = item.GetUserDataKeys().FirstOrDefault();
                 var inheritedTags = item.GetInheritedTags();
 
-                tuples.Add(new Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>(item, ancestorIds, topParent, userdataKey, inheritedTags));
+                tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
             }
 
             using (WriteLock.Write())
@@ -615,7 +615,7 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        private void SaveItemsInTranscation(IDatabaseConnection db, List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>> tuples)
+        private void SaveItemsInTranscation(IDatabaseConnection db, IEnumerable<(BaseItem, List<Guid>, BaseItem, string, List<string>)> tuples)
         {
             var statements = PrepareAllSafe(db, new string[]
             {
@@ -966,7 +966,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.ExtraIds.Length > 0)
             {
-                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds.ToArray()));
+                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
             }
             else
             {
@@ -1183,9 +1183,9 @@ namespace Emby.Server.Implementations.Data
         /// <exception cref="ArgumentException"></exception>
         public BaseItem RetrieveItem(Guid id)
         {
-            if (id.Equals(Guid.Empty))
+            if (id == Guid.Empty)
             {
-                throw new ArgumentNullException(nameof(id));
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
             }
 
             CheckDisposed();
@@ -2079,14 +2079,14 @@ namespace Emby.Server.Implementations.Data
                 return false;
             }
 
-            var sortingFields = query.OrderBy.Select(i => i.Item1);
+            var sortingFields = new HashSet<string>(query.OrderBy.Select(i => i.Item1), StringComparer.OrdinalIgnoreCase);
 
-            return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.SeriesDatePlayed, StringComparer.OrdinalIgnoreCase)
+            return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
+                    || sortingFields.Contains(ItemSortBy.IsPlayed)
+                    || sortingFields.Contains(ItemSortBy.IsUnplayed)
+                    || sortingFields.Contains(ItemSortBy.PlayCount)
+                    || sortingFields.Contains(ItemSortBy.DatePlayed)
+                    || sortingFields.Contains(ItemSortBy.SeriesDatePlayed)
                     || query.IsFavoriteOrLiked.HasValue
                     || query.IsFavorite.HasValue
                     || query.IsResumable.HasValue
@@ -2094,9 +2094,9 @@ namespace Emby.Server.Implementations.Data
                     || query.IsLiked.HasValue;
         }
 
-        private readonly List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields))
+        private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields))
             .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
-            .ToList();
+            .ToArray();
 
         private string[] GetColumnNamesFromField(ItemFields field)
         {
@@ -2151,18 +2151,26 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        private bool HasProgramAttributes(InternalItemsQuery query)
+        private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "Series",
+            "Season",
+            "MusicAlbum",
+            "MusicArtist",
+            "PhotoAlbum"
+        };
+
+        private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Program",
+            "TvChannel",
+            "LiveTvProgram",
+            "LiveTvTvChannel"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasProgramAttributes(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2172,29 +2180,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Program",
-                "TvChannel",
-                "LiveTvProgram",
-                "LiveTvTvChannel"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
         }
 
-        private bool HasServiceName(InternalItemsQuery query)
+        private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "TvChannel",
+            "LiveTvTvChannel"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasServiceName(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2204,27 +2201,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "TvChannel",
-                "LiveTvTvChannel"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
         }
 
-        private bool HasStartDate(InternalItemsQuery query)
+        private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "Program",
+            "LiveTvProgram"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasStartDate(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2234,13 +2222,7 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Program",
-                "LiveTvProgram"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x));
         }
 
         private bool HasEpisodeAttributes(InternalItemsQuery query)
@@ -2263,16 +2245,26 @@ namespace Emby.Server.Implementations.Data
             return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
         }
 
-        private bool HasArtistFields(InternalItemsQuery query)
+
+        private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "PhotoAlbum"
-            };
+            "Series",
+            "Season",
+            "PhotoAlbum"
+        };
+
+        private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Audio",
+            "MusicAlbum",
+            "MusicVideo",
+            "AudioBook",
+            "AudioPodcast"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasArtistFields(InternalItemsQuery query)
+        {
+            if (_artistExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2282,18 +2274,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Audio",
-                "MusicAlbum",
-                "MusicVideo",
-                "AudioBook",
-                "AudioPodcast"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
         }
 
+        private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Audio",
+            "MusicAlbum",
+            "MusicVideo",
+            "AudioBook",
+            "AudioPodcast"
+        };
+
         private bool HasSeriesFields(InternalItemsQuery query)
         {
             if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@@ -2306,26 +2298,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Book",
-                "AudioBook",
-                "Episode",
-                "Season"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
         }
 
-        private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns)
+        private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
         {
             var list = startColumns.ToList();
 
-            foreach (var field in allFields)
+            foreach (var field in _allFields)
             {
                 if (!HasField(query, field))
                 {
-                    foreach (var fieldToRemove in GetColumnNamesFromField(field).ToList())
+                    foreach (var fieldToRemove in GetColumnNamesFromField(field))
                     {
                         list.Remove(fieldToRemove);
                     }
@@ -2419,11 +2403,14 @@ namespace Emby.Server.Implementations.Data
 
                 list.Add(builder.ToString());
 
-                var excludeIds = query.ExcludeItemIds.ToList();
-                excludeIds.Add(item.Id);
-                excludeIds.AddRange(item.ExtraIds);
+                var oldLen = query.ExcludeItemIds.Length;
+                var newLen = oldLen + item.ExtraIds.Length + 1;
+                var excludeIds = new Guid[newLen];
+                query.ExcludeItemIds.CopyTo(excludeIds, 0);
+                excludeIds[oldLen] = item.Id;
+                item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
 
-                query.ExcludeItemIds = excludeIds.ToArray();
+                query.ExcludeItemIds = excludeIds;
                 query.ExcludeProviderIds = item.ProviderIds;
             }
 
@@ -2444,7 +2431,7 @@ namespace Emby.Server.Implementations.Data
                 list.Add(builder.ToString());
             }
 
-            return list.ToArray();
+            return list;
         }
 
         private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2723,18 +2710,17 @@ namespace Emby.Server.Implementations.Data
 
         private void AddItem(List<BaseItem> items, BaseItem newItem)
         {
-            var providerIds = newItem.ProviderIds.ToList();
-
             for (var i = 0; i < items.Count; i++)
             {
                 var item = items[i];
 
-                foreach (var providerId in providerIds)
+                foreach (var providerId in newItem.ProviderIds)
                 {
                     if (providerId.Key == MetadataProviders.TmdbCollection.ToString())
                     {
                         continue;
                     }
+
                     if (item.GetProviderId(providerId.Key) == providerId.Value)
                     {
                         if (newItem.SourceType == SourceType.Library)
@@ -2753,10 +2739,10 @@ namespace Emby.Server.Implementations.Data
         {
             var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
 
-            int slowThreshold = 1000;
+            int slowThreshold = 100;
 
 #if DEBUG
-            slowThreshold = 250;
+            slowThreshold = 10;
 #endif
 
             if (elapsed >= slowThreshold)
@@ -2806,7 +2792,7 @@ namespace Emby.Server.Implementations.Data
 
             var whereText = whereClauses.Count == 0 ?
                 string.Empty :
-                " where " + string.Join(" AND ", whereClauses.ToArray());
+                " where " + string.Join(" AND ", whereClauses);
 
             commandText += whereText
                         + GetGroupBy(query)
@@ -2930,25 +2916,31 @@ namespace Emby.Server.Implementations.Data
 
         private string GetOrderByText(InternalItemsQuery query)
         {
-            var orderBy = query.OrderBy.ToList();
-            var enableOrderInversion = false;
-
-            if (query.SimilarTo != null && orderBy.Count == 0)
+            if (string.IsNullOrEmpty(query.SearchTerm))
             {
-                orderBy.Add(new ValueTuple<string, SortOrder>("SimilarityScore", SortOrder.Descending));
-                orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
-            }
+                int oldLen = query.OrderBy.Length;
 
-            if (!string.IsNullOrEmpty(query.SearchTerm))
+                if (query.SimilarTo != null && oldLen == 0)
+                {
+                    var arr = new (string, SortOrder)[oldLen + 2];
+                    query.OrderBy.CopyTo(arr, 0);
+                    arr[oldLen] = ("SimilarityScore", SortOrder.Descending);
+                    arr[oldLen + 1] = (ItemSortBy.Random, SortOrder.Ascending);
+                    query.OrderBy = arr;
+                }
+            }
+            else
             {
-                orderBy = new List<(string, SortOrder)>();
-                orderBy.Add(new ValueTuple<string, SortOrder>("SearchScore", SortOrder.Descending));
-                orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending));
+                query.OrderBy = new []
+                {
+                    ("SearchScore", SortOrder.Descending),
+                    (ItemSortBy.SortName, SortOrder.Ascending)
+                };
             }
 
-            query.OrderBy = orderBy.ToArray();
+            var orderBy = query.OrderBy;
 
-            if (orderBy.Count == 0)
+            if (orderBy.Length == 0)
             {
                 return string.Empty;
             }
@@ -2957,6 +2949,7 @@ namespace Emby.Server.Implementations.Data
             {
                 var columnMap = MapOrderByField(i.Item1, query);
                 var columnAscending = i.Item2 == SortOrder.Ascending;
+                const bool enableOrderInversion = false;
                 if (columnMap.Item2 && enableOrderInversion)
                 {
                     columnAscending = !columnAscending;
@@ -2968,7 +2961,7 @@ namespace Emby.Server.Implementations.Data
             }));
         }
 
-        private ValueTuple<string, bool> MapOrderByField(string name, InternalItemsQuery query)
+        private (string, bool) MapOrderByField(string name, InternalItemsQuery query)
         {
             if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
             {
@@ -3218,7 +3211,7 @@ namespace Emby.Server.Implementations.Data
 
             var whereText = whereClauses.Count == 0 ?
                 string.Empty :
-                " where " + string.Join(" AND ", whereClauses.ToArray());
+                " where " + string.Join(" AND ", whereClauses);
 
             commandText += whereText
                         + GetGroupBy(query)
@@ -4378,7 +4371,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (query.Years.Length > 1)
             {
-                var val = string.Join(",", query.Years.ToArray());
+                var val = string.Join(",", query.Years);
 
                 whereClauses.Add("ProductionYear in (" + val + ")");
             }
@@ -4952,7 +4945,12 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 return result;
             }
 
-            return new[] { value }.Where(IsValidType);
+            if (IsValidType(value))
+            {
+                return new[] { value };
+            }
+
+            return Array.Empty<string>();
         }
 
         public void DeleteItem(Guid id, CancellationToken cancellationToken)
@@ -5215,32 +5213,32 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
         }
@@ -5317,7 +5315,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
         }
 
-        private QueryResult<Tuple<BaseItem, ItemCounts>> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
+        private QueryResult<(BaseItem, ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
         {
             if (query == null)
             {
@@ -5335,7 +5333,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")");
+                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
             InternalItemsQuery typeSubQuery = null;
 
@@ -5363,11 +5361,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                 whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
 
-                var typeWhereText = whereClauses.Count == 0 ?
-                    string.Empty :
-                    " where " + string.Join(" AND ", whereClauses);
-
-                itemCountColumnQuery += typeWhereText;
+                itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
 
                 itemCountColumns = new Dictionary<string, string>()
                 {
@@ -5400,7 +5394,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 IsSeries = query.IsSeries
             };
 
-            columns = GetFinalColumnsToSelect(query, columns.ToArray()).ToList();
+            columns = GetFinalColumnsToSelect(query, columns);
 
             var commandText = "select "
                             + string.Join(",", columns)
@@ -5492,8 +5486,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 {
                     return connection.RunInTransaction(db =>
                     {
-                        var list = new List<Tuple<BaseItem, ItemCounts>>();
-                        var result = new QueryResult<Tuple<BaseItem, ItemCounts>>();
+                        var list = new List<(BaseItem, ItemCounts)>();
+                        var result = new QueryResult<(BaseItem, ItemCounts)>();
 
                         var statements = PrepareAllSafe(db, statementTexts);
 
@@ -5531,7 +5525,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                                     {
                                         var countStartColumn = columns.Count - 1;
 
-                                        list.Add(new Tuple<BaseItem, ItemCounts>(item, GetItemCounts(row, countStartColumn, typesToCount)));
+                                        list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
                                     }
                                 }
 
@@ -6198,6 +6192,5 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             return item;
         }
-
     }
 }

+ 17 - 17
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
         {
-            var result = new StreamWriter(content, contentType, _logger);
+            var result = new StreamWriter(content, contentType);
 
             if (responseHeaders == null)
             {
@@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.HttpServer
                     content = Array.Empty<byte>();
                 }
 
-                result = new StreamWriter(content, contentType, contentLength, _logger);
+                result = new StreamWriter(content, contentType, contentLength);
             }
             else
             {
@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders = new Dictionary<string, string>();
             }
 
-            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
+            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
             {
                 responseHeaders["Expires"] = "-1";
             }
@@ -175,7 +175,7 @@ namespace Emby.Server.Implementations.HttpServer
                     bytes = Array.Empty<byte>();
                 }
 
-                result = new StreamWriter(bytes, contentType, contentLength, _logger);
+                result = new StreamWriter(bytes, contentType, contentLength);
             }
             else
             {
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders = new Dictionary<string, string>();
             }
 
-            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
+            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
             {
                 responseHeaders["Expires"] = "-1";
             }
@@ -277,9 +277,10 @@ namespace Emby.Server.Implementations.HttpServer
 
         private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
         {
-            var contentType = request.ResponseContentType;
+            // TODO: @bond use Span and .Equals
+            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
 
-            switch (GetRealContentType(contentType))
+            switch (contentType)
             {
                 case "application/xml":
                 case "text/xml":
@@ -333,13 +334,13 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (isHeadRequest)
             {
-                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger);
+                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
                 AddResponseHeaders(result, responseHeaders);
                 return result;
             }
             else
             {
-                var result = new StreamWriter(content, contentType, contentLength, _logger);
+                var result = new StreamWriter(content, contentType, contentLength);
                 AddResponseHeaders(result, responseHeaders);
                 return result;
             }
@@ -348,13 +349,19 @@ namespace Emby.Server.Implementations.HttpServer
         private byte[] Compress(byte[] bytes, string compressionType)
         {
             if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
+            {
                 return CompressBrotli(bytes);
+            }
 
             if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
+            {
                 return Deflate(bytes);
+            }
 
             if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
+            {
                 return GZip(bytes);
+            }
 
             throw new NotSupportedException(compressionType);
         }
@@ -390,13 +397,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        public static string GetRealContentType(string contentType)
-        {
-            return contentType == null
-                       ? null
-                       : contentType.Split(';')[0].ToLowerInvariant().Trim();
-        }
-
         private static string SerializeToXmlString(object from)
         {
             using (var ms = new MemoryStream())
@@ -603,7 +603,7 @@ namespace Emby.Server.Implementations.HttpServer
                     }
                 }
 
-                var hasHeaders = new StreamWriter(stream, contentType, _logger)
+                var hasHeaders = new StreamWriter(stream, contentType)
                 {
                     OnComplete = options.OnComplete,
                     OnError = options.OnError

+ 2 - 6
Emby.Server.Implementations/HttpServer/StreamWriter.cs

@@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
     /// </summary>
     public class StreamWriter : IAsyncStreamWriter, IHasHeaders
     {
-        private ILogger Logger { get; set; }
-
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
         /// <summary>
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="logger">The logger.</param>
-        public StreamWriter(Stream source, string contentType, ILogger logger)
+        public StreamWriter(Stream source, string contentType)
         {
             if (string.IsNullOrEmpty(contentType))
             {
@@ -53,7 +51,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
 
             SourceStream = source;
-            Logger = logger;
 
             Headers["Content-Type"] = contentType;
 
@@ -69,7 +66,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="logger">The logger.</param>
-        public StreamWriter(byte[] source, string contentType, int contentLength, ILogger logger)
+        public StreamWriter(byte[] source, string contentType, int contentLength)
         {
             if (string.IsNullOrEmpty(contentType))
             {
@@ -77,7 +74,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
 
             SourceBytes = source;
-            Logger = logger;
 
             Headers["Content-Type"] = contentType;
 

+ 21 - 23
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1225,9 +1225,9 @@ namespace Emby.Server.Implementations.Library
         /// <exception cref="ArgumentNullException">id</exception>
         public BaseItem GetItemById(Guid id)
         {
-            if (id.Equals(Guid.Empty))
+            if (id == Guid.Empty)
             {
-                throw new ArgumentNullException(nameof(id));
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
             }
 
             if (LibraryItemsCache.TryGetValue(id, out BaseItem item))
@@ -1237,8 +1237,6 @@ namespace Emby.Server.Implementations.Library
 
             item = RetrieveItem(id);
 
-            //_logger.LogDebug("GetitemById {0}", id);
-
             if (item != null)
             {
                 RegisterItem(item);
@@ -1333,7 +1331,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetItemIdsList(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1344,7 +1342,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetStudios(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1355,7 +1353,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetGenres(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1366,7 +1364,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetMusicGenres(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1377,7 +1375,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetAllArtists(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1421,7 +1419,7 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1808,18 +1806,16 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task.</returns>
         public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
         {
-            var list = items.ToList();
-
-            ItemRepository.SaveItems(list, cancellationToken);
+            ItemRepository.SaveItems(items, cancellationToken);
 
-            foreach (var item in list)
+            foreach (var item in items)
             {
                 RegisterItem(item);
             }
 
             if (ItemAdded != null)
             {
-                foreach (var item in list)
+                foreach (var item in items)
                 {
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
@@ -1853,7 +1849,7 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        public void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
             foreach (var item in items)
             {
@@ -1908,7 +1904,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task.</returns>
         public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
-            UpdateItems(new List<BaseItem> { item }, parent, updateReason, cancellationToken);
+            UpdateItems(new [] { item }, parent, updateReason, cancellationToken);
         }
 
         /// <summary>
@@ -2005,9 +2001,7 @@ namespace Emby.Server.Implementations.Library
                    .FirstOrDefault();
             }
 
-            var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
-
-            return options;
+            return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
         }
 
         public string GetContentType(BaseItem item)
@@ -2017,11 +2011,13 @@ namespace Emby.Server.Implementations.Library
             {
                 return configuredContentType;
             }
+
             configuredContentType = GetConfiguredContentType(item, true);
             if (!string.IsNullOrEmpty(configuredContentType))
             {
                 return configuredContentType;
             }
+
             return GetInheritedContentType(item);
         }
 
@@ -2056,6 +2052,7 @@ namespace Emby.Server.Implementations.Library
             {
                 return collectionFolder.CollectionType;
             }
+
             return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
         }
 
@@ -2066,6 +2063,7 @@ namespace Emby.Server.Implementations.Library
             {
                 return nameValuePair.Value;
             }
+
             return null;
         }
 
@@ -2108,9 +2106,9 @@ namespace Emby.Server.Implementations.Library
             string viewType,
             string sortName)
         {
-            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views");
-
-            path = Path.Combine(path, _fileSystem.GetValidFilename(viewType));
+            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath,
+                                    "views",
+                                    _fileSystem.GetValidFilename(viewType));
 
             var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 

+ 2 - 2
Emby.Server.Implementations/Library/UserManager.cs

@@ -168,9 +168,9 @@ namespace Emby.Server.Implementations.Library
         /// <exception cref="ArgumentNullException"></exception>
         public User GetUserById(Guid id)
         {
-            if (id.Equals(Guid.Empty))
+            if (id == Guid.Empty)
             {
-                throw new ArgumentNullException(nameof(id));
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
             }
 
             return Users.FirstOrDefault(u => u.Id == id);

+ 1 - 1
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.LiveTv
 
         public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
         {
-            var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId);
+            var user = query.UserId == Guid.Empty ? null : _userManager.GetUserById(query.UserId);
 
             var topFolder = GetInternalLiveTvFolder(cancellationToken);
 

+ 21 - 0
Emby.Server.Implementations/Serialization/JsonSerializer.cs

@@ -41,6 +41,27 @@ namespace Emby.Server.Implementations.Serialization
             ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream);
         }
 
+        /// <summary>
+        /// Serializes to stream.
+        /// </summary>
+        /// <param name="obj">The obj.</param>
+        /// <param name="stream">The stream.</param>
+        /// <exception cref="ArgumentNullException">obj</exception>
+        public void SerializeToStream<T>(T obj, Stream stream)
+        {
+            if (obj == null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            if (stream == null)
+            {
+                throw new ArgumentNullException(nameof(stream));
+            }
+
+            ServiceStack.Text.JsonSerializer.SerializeToStream<T>(obj, stream);
+        }
+
         /// <summary>
         /// Serializes to file.
         /// </summary>

+ 32 - 35
Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs

@@ -28,30 +28,6 @@ namespace Jellyfin.Server.SocketSharp
             // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
         }
 
-        private static string GetHandlerPathIfAny(string listenerUrl)
-        {
-            if (listenerUrl == null)
-            {
-                return null;
-            }
-
-            var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase);
-            if (pos == -1)
-            {
-                return null;
-            }
-
-            var startHostUrl = listenerUrl.Substring(pos + "://".Length);
-            var endPos = startHostUrl.IndexOf('/', StringComparison.Ordinal);
-            if (endPos == -1)
-            {
-                return null;
-            }
-
-            var endHostUrl = startHostUrl.Substring(endPos + 1);
-            return string.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/');
-        }
-
         public HttpListenerRequest HttpRequest => request;
 
         public object OriginalRequest => request;
@@ -102,7 +78,7 @@ namespace Jellyfin.Server.SocketSharp
             name = name.Trim(HttpTrimCharacters);
 
             // First, check for correctly formed multi-line value
-            // Second, check for absenece of CTL characters
+            // Second, check for absence of CTL characters
             int crlf = 0;
             for (int i = 0; i < name.Length; ++i)
             {
@@ -231,8 +207,15 @@ namespace Jellyfin.Server.SocketSharp
             {
                 foreach (var acceptsType in acceptContentTypes)
                 {
-                    var contentType = HttpResultFactory.GetRealContentType(acceptsType);
-                    acceptsAnything = acceptsAnything || contentType == "*/*";
+                    // TODO: @bond move to Span when Span.Split lands
+                    // https://github.com/dotnet/corefx/issues/26528
+                    var contentType = acceptsType?.Split(';')[0].Trim();
+                    acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
+
+                    if (acceptsAnything)
+                    {
+                        break;
+                    }
                 }
 
                 if (acceptsAnything)
@@ -241,7 +224,7 @@ namespace Jellyfin.Server.SocketSharp
                     {
                         return defaultContentType;
                     }
-                    else if (serverDefaultContentType != null)
+                    else
                     {
                         return serverDefaultContentType;
                     }
@@ -284,11 +267,11 @@ namespace Jellyfin.Server.SocketSharp
 
         private static string GetQueryStringContentType(IRequest httpReq)
         {
-            var format = httpReq.QueryString["format"];
+            ReadOnlySpan<char> format = httpReq.QueryString["format"];
             if (format == null)
             {
                 const int formatMaxLength = 4;
-                var pi = httpReq.PathInfo;
+                ReadOnlySpan<char> pi = httpReq.PathInfo;
                 if (pi == null || pi.Length <= formatMaxLength)
                 {
                     return null;
@@ -296,7 +279,7 @@ namespace Jellyfin.Server.SocketSharp
 
                 if (pi[0] == '/')
                 {
-                    pi = pi.Substring(1);
+                    pi = pi.Slice(1);
                 }
 
                 format = LeftPart(pi, '/');
@@ -330,6 +313,17 @@ namespace Jellyfin.Server.SocketSharp
             return pos == -1 ? strVal : strVal.Substring(0, pos);
         }
 
+        public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
+        {
+            if (strVal == null)
+            {
+                return null;
+            }
+
+            var pos = strVal.IndexOf(needle);
+            return pos == -1 ? strVal : strVal.Slice(0, pos);
+        }
+
         public static string HandlerFactoryPath;
 
         private string pathInfo;
@@ -341,7 +335,7 @@ namespace Jellyfin.Server.SocketSharp
                 {
                     var mode = HandlerFactoryPath;
 
-                    var pos = request.RawUrl.IndexOf("?", StringComparison.Ordinal);
+                    var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal);
                     if (pos != -1)
                     {
                         var path = request.RawUrl.Substring(0, pos);
@@ -525,10 +519,13 @@ namespace Jellyfin.Server.SocketSharp
 
         public static string NormalizePathInfo(string pathInfo, string handlerPath)
         {
-            var trimmed = pathInfo.TrimStart('/');
-            if (handlerPath != null && trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+            if (handlerPath != null)
             {
-                return trimmed.Substring(handlerPath.Length);
+                var trimmed = pathInfo.TrimStart('/');
+                if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+                {
+                    return trimmed.Substring(handlerPath.Length);
+                }
             }
 
             return pathInfo;

+ 23 - 9
MediaBrowser.Api/BaseApiService.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api
@@ -118,8 +119,7 @@ namespace MediaBrowser.Api
         {
             var options = new DtoOptions();
 
-            var hasFields = request as IHasItemFields;
-            if (hasFields != null)
+            if (request is IHasItemFields hasFields)
             {
                 options.Fields = hasFields.GetItemFields();
             }
@@ -133,9 +133,11 @@ namespace MediaBrowser.Api
                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    var list = options.Fields.ToList();
-                    list.Add(Model.Querying.ItemFields.RecursiveItemCount);
-                    options.Fields = list.ToArray();
+                    int oldLen = options.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    options.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = Model.Querying.ItemFields.RecursiveItemCount;
+                    options.Fields = arr;
                 }
 
                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
@@ -146,9 +148,12 @@ namespace MediaBrowser.Api
                    client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
                    client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    var list = options.Fields.ToList();
-                    list.Add(Model.Querying.ItemFields.ChildCount);
-                    options.Fields = list.ToArray();
+
+                    int oldLen = options.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    options.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = Model.Querying.ItemFields.ChildCount;
+                    options.Fields = arr;
                 }
             }
 
@@ -167,7 +172,16 @@ namespace MediaBrowser.Api
 
                 if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
                 {
-                    options.ImageTypes = (hasDtoOptions.EnableImageTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToArray();
+                    if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
+                    {
+                        options.ImageTypes = Array.Empty<ImageType>();
+                    }
+                    else
+                    {
+                        options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                                                                            .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+                                                                            .ToArray();
+                    }
                 }
             }
 

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

@@ -112,7 +112,7 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(result);
         }
 
-        protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
             if (request is GetAlbumArtists)
             {

+ 2 - 2
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -209,9 +209,9 @@ namespace MediaBrowser.Api.UserLibrary
             };
         }
 
-        protected virtual QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected virtual QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
-            return new QueryResult<Tuple<BaseItem, ItemCounts>>();
+            return new QueryResult<(BaseItem, ItemCounts)>();
         }
 
         private void SetItemCounts(BaseItemDto dto, ItemCounts counts)

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

@@ -396,14 +396,12 @@ namespace MediaBrowser.Api.UserLibrary
 
         public VideoType[] GetVideoTypes()
         {
-            var val = VideoTypes;
-
-            if (string.IsNullOrEmpty(val))
+            if (string.IsNullOrEmpty(VideoTypes))
             {
-                return new VideoType[] { };
+                return Array.Empty<VideoType>();
             }
 
-            return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
+            return VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
         }
 
         /// <summary>

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

@@ -92,7 +92,7 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(result);
         }
 
-        protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
             var viewType = GetParentItemViewType(request);
 

+ 7 - 10
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -90,7 +90,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var options = GetDtoOptions(_authContext, request);
 
-            var ancestorIds = new List<Guid>();
+            var ancestorIds = Array.Empty<Guid>();
 
             var excludeFolderIds = user.Configuration.LatestItemsExcludes;
             if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
@@ -99,12 +99,12 @@ namespace MediaBrowser.Api.UserLibrary
                     .Where(i => i is Folder)
                     .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N")))
                     .Select(i => i.Id)
-                    .ToList();
+                    .ToArray();
             }
 
             var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
             {
-                OrderBy = new[] { ItemSortBy.DatePlayed }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+                OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
                 IsResumable = true,
                 StartIndex = request.StartIndex,
                 Limit = request.Limit,
@@ -115,7 +115,7 @@ namespace MediaBrowser.Api.UserLibrary
                 IsVirtualItem = false,
                 CollapseBoxSetItems = false,
                 EnableTotalRecordCount = request.EnableTotalRecordCount,
-                AncestorIds = ancestorIds.ToArray(),
+                AncestorIds = ancestorIds,
                 IncludeItemTypes = request.GetIncludeItemTypes(),
                 ExcludeItemTypes = request.GetExcludeItemTypes(),
                 SearchTerm = request.SearchTerm
@@ -155,7 +155,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="request">The request.</param>
         private QueryResult<BaseItemDto> GetItems(GetItems request)
         {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
+            var user = request.UserId == Guid.Empty ? null : _userManager.GetUserById(request.UserId);
 
             var dtoOptions = GetDtoOptions(_authContext, request);
 
@@ -190,11 +190,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         private QueryResult<BaseItem> GetQueryResult(GetItems request, DtoOptions dtoOptions, User user)
         {
-            if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase))
-            {
-                request.ParentId = null;
-            }
-            else if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
             {
                 request.ParentId = null;
             }

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

@@ -83,7 +83,7 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(result);
         }
 
-        protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
             return LibraryManager.GetMusicGenres(query);
         }

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

@@ -101,7 +101,7 @@ namespace MediaBrowser.Api.UserLibrary
             throw new NotImplementedException();
         }
 
-        protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
             var items = LibraryManager.GetPeopleItems(new InternalPeopleQuery
             {
@@ -109,10 +109,10 @@ namespace MediaBrowser.Api.UserLibrary
                 NameContains = query.NameContains ?? query.SearchTerm
             });
 
-            return new QueryResult<Tuple<BaseItem, ItemCounts>>
+            return new QueryResult<(BaseItem, ItemCounts)>
             {
                 TotalRecordCount = items.Count,
-                Items = items.Take(query.Limit ?? int.MaxValue).Select(i => new Tuple<BaseItem, ItemCounts>(i, new ItemCounts())).ToArray()
+                Items = items.Take(query.Limit ?? int.MaxValue).Select(i => (i as BaseItem, new ItemCounts())).ToArray()
             };
         }
 

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

@@ -91,7 +91,7 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(result);
         }
 
-        protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query)
+        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
         {
             return LibraryManager.GetStudios(query);
         }

+ 7 - 7
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -193,7 +193,7 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+        void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
         void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
 
         /// <summary>
@@ -520,12 +520,12 @@ namespace MediaBrowser.Controller.Library
         void UpdateMediaPath(string virtualFolderName, MediaPathInfo path);
         void RemoveMediaPath(string virtualFolderName, string path);
 
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query);
 
         int GetCount(InternalItemsQuery query);
 

+ 7 - 7
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Persistence
         /// </summary>
         /// <param name="items">The items.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        void SaveItems(List<BaseItem> items, CancellationToken cancellationToken);
+        void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken);
 
         void SaveImages(BaseItem item);
 
@@ -141,12 +141,12 @@ namespace MediaBrowser.Controller.Persistence
 
         int GetCount(InternalItemsQuery query);
 
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
-        QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
+        QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query);
 
         List<string> GetMusicGenreNames();
         List<string> GetStudioNames();

+ 8 - 0
MediaBrowser.Model/Serialization/IJsonSerializer.cs

@@ -14,6 +14,14 @@ namespace MediaBrowser.Model.Serialization
         /// <exception cref="ArgumentNullException">obj</exception>
         void SerializeToStream(object obj, Stream stream);
 
+        /// <summary>
+        /// Serializes to stream.
+        /// </summary>
+        /// <param name="obj">The obj.</param>
+        /// <param name="stream">The stream.</param>
+        /// <exception cref="ArgumentNullException">obj</exception>
+        void SerializeToStream<T>(T obj, Stream stream);
+
         /// <summary>
         /// Serializes to file.
         /// </summary>