Browse Source

Merge branch 'master' into tonemap

Nyanmisaka 4 years ago
parent
commit
c23d991c95
78 changed files with 1927 additions and 608 deletions
  1. 9 1
      .ci/azure-pipelines-package.yml
  2. 1 0
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  3. 9 9
      Emby.Dlna/DlnaManager.cs
  4. 0 3
      Emby.Server.Implementations/ApplicationHost.cs
  5. 10 14
      Emby.Server.Implementations/Channels/ChannelManager.cs
  6. 0 225
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  7. 21 113
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  8. 12 11
      Emby.Server.Implementations/Devices/DeviceManager.cs
  9. 2 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  10. 1 1
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  11. 1 1
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  12. 1 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  13. 9 10
      Emby.Server.Implementations/Library/LibraryManager.cs
  14. 1 1
      Emby.Server.Implementations/Library/MusicManager.cs
  15. 1 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  16. 1 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  17. 2 2
      Emby.Server.Implementations/Localization/Core/de.json
  18. 1 1
      Emby.Server.Implementations/Networking/NetworkManager.cs
  19. 150 0
      Jellyfin.Data/Entities/DisplayPreferences.cs
  20. 46 0
      Jellyfin.Data/Entities/HomeSection.cs
  21. 120 0
      Jellyfin.Data/Entities/ItemDisplayPreferences.cs
  22. 15 0
      Jellyfin.Data/Entities/User.cs
  23. 18 0
      Jellyfin.Data/Enums/ChromecastVersion.cs
  24. 53 0
      Jellyfin.Data/Enums/HomeSectionType.cs
  25. 20 0
      Jellyfin.Data/Enums/IndexingKind.cs
  26. 18 0
      Jellyfin.Data/Enums/ScrollDirection.cs
  27. 18 0
      Jellyfin.Data/Enums/SortOrder.cs
  28. 38 0
      Jellyfin.Data/Enums/ViewType.cs
  29. 4 5
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  30. 56 5
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  31. 6 8
      Jellyfin.Drawing.Skia/StripCollageBuilder.cs
  32. 4 0
      Jellyfin.Server.Implementations/JellyfinDb.cs
  33. 11 3
      Jellyfin.Server.Implementations/JellyfinDbProvider.cs
  34. 459 0
      Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
  35. 132 0
      Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
  36. 148 1
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  37. 4 10
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  38. 88 0
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  39. 1 6
      Jellyfin.Server.Implementations/Users/UserManager.cs
  40. 7 5
      Jellyfin.Server/CoreAppHost.cs
  41. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  42. 174 0
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  43. 1 1
      MediaBrowser.Api/ChannelService.cs
  44. 108 20
      MediaBrowser.Api/DisplayPreferencesService.cs
  45. 1 0
      MediaBrowser.Api/Movies/MoviesService.cs
  46. 2 1
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  47. 9 5
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  48. 9 6
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  49. 1 0
      MediaBrowser.Api/SuggestionsService.cs
  50. 1 0
      MediaBrowser.Api/TvShowsService.cs
  51. 3 2
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  52. 1 0
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  53. 49 0
      MediaBrowser.Controller/IDisplayPreferencesManager.cs
  54. 1 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  55. 0 53
      MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
  56. 1 0
      MediaBrowser.Controller/Playlists/Playlist.cs
  57. 8 3
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  58. 6 6
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  59. 1 1
      MediaBrowser.Model/Dlna/SortCriteria.cs
  60. 4 8
      MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
  61. 0 18
      MediaBrowser.Model/Entities/ScrollDirection.cs
  62. 0 18
      MediaBrowser.Model/Entities/SortOrder.cs
  63. 1 1
      MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
  64. 1 1
      MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
  65. 3 3
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  66. 1 1
      MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
  67. 2 2
      MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
  68. 2 2
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
  69. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
  70. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  71. 16 6
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  72. 2 1
      README.md
  73. 2 2
      SharedVersion.cs
  74. 1 1
      build.yaml
  75. 2 1
      bump_version
  76. 6 0
      debian/changelog
  77. 1 1
      debian/metapackage/jellyfin
  78. 3 1
      fedora/jellyfin.spec

+ 9 - 1
.ci/azure-pipelines-package.yml

@@ -80,7 +80,15 @@ jobs:
   pool:
   pool:
     vmImage: 'ubuntu-latest'
     vmImage: 'ubuntu-latest'
 
 
+  variables:
+  - name: JellyfinVersion
+    value: 0.0.0
+
   steps:
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+
   - task: Docker@2
   - task: Docker@2
     displayName: 'Push Unstable Image'
     displayName: 'Push Unstable Image'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -105,7 +113,7 @@ jobs:
       containerRegistry: Docker Hub
       containerRegistry: Docker Hub
       tags: |
       tags: |
         stable-$(Build.BuildNumber)-$(BuildConfiguration)
         stable-$(Build.BuildNumber)-$(BuildConfiguration)
-        stable-$(BuildConfiguration)
+        $(JellyfinVersion)-$(BuildConfiguration)
 
 
 - job: CollectArtifacts
 - job: CollectArtifacts
   displayName: 'Collect Artifacts'
   displayName: 'Collect Artifacts'

+ 1 - 0
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -11,6 +11,7 @@ using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;

+ 9 - 9
Emby.Dlna/DlnaManager.cs

@@ -122,15 +122,15 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
             var builder = new StringBuilder();
 
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
-            builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
-            builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
-            builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
-            builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
-            builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
-            builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
-            builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
-            builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+            builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
+            builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
 
 
             _logger.LogInformation(builder.ToString());
             _logger.LogInformation(builder.ToString());
         }
         }

+ 0 - 3
Emby.Server.Implementations/ApplicationHost.cs

@@ -554,8 +554,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
 
-            serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
-
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -650,7 +648,6 @@ namespace Emby.Server.Implementations
             _httpServer = Resolve<IHttpServer>();
             _httpServer = Resolve<IHttpServer>();
             _httpClient = Resolve<IHttpClient>();
             _httpClient = Resolve<IHttpClient>();
 
 
-            ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
 
             SetStaticProperties();
             SetStaticProperties();

+ 10 - 14
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -7,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
@@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
-
-        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
-            new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
+        private readonly IMemoryCache _memoryCache;
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
 
 
         /// <summary>
         /// <summary>
@@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="providerManager">The provider manager.</param>
+        /// <param name="memoryCache">The memory cache.</param>
         public ChannelManager(
         public ChannelManager(
             IUserManager userManager,
             IUserManager userManager,
             IDtoService dtoService,
             IDtoService dtoService,
@@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IProviderManager providerManager)
+            IProviderManager providerManager,
+            IMemoryCache memoryCache)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _dtoService = dtoService;
             _dtoService = dtoService;
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _providerManager = providerManager;
             _providerManager = providerManager;
+            _memoryCache = memoryCache;
         }
         }
 
 
         internal IChannel[] Channels { get; private set; }
         internal IChannel[] Channels { get; private set; }
@@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
 
 
         private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
         private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
         {
         {
-            if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
+            if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
             {
             {
-                if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
-                {
-                    return cachedInfo.Item2;
-                }
+                return cachedInfo;
             }
             }
 
 
             var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
             var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
                    .ConfigureAwait(false);
                    .ConfigureAwait(false);
             var list = mediaInfo.ToList();
             var list = mediaInfo.ToList();
-
-            var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
-            _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+            _memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
 
 
             return list;
             return list;
         }
         }

+ 0 - 225
Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs

@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    /// <summary>
-    /// Class SQLiteDisplayPreferencesRepository.
-    /// </summary>
-    public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
-    {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly JsonSerializerOptions _jsonOptions;
-
-        public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
-            : base(logger)
-        {
-            _fileSystem = fileSystem;
-
-            _jsonOptions = JsonDefaults.GetOptions();
-
-            DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
-        }
-
-        /// <summary>
-        /// Gets the name of the repository.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => "SQLite";
-
-        public void Initialize()
-        {
-            try
-            {
-                InitializeInternal();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
-                _fileSystem.DeleteFile(DbFilePath);
-
-                InitializeInternal();
-            }
-        }
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        /// <returns>Task.</returns>
-        private void InitializeInternal()
-        {
-            string[] queries =
-            {
-                "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
-                "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
-            };
-
-            using (var connection = GetConnection())
-            {
-                connection.RunQueries(queries);
-            }
-        }
-
-        /// <summary>
-        /// Save the display preferences associated with an item in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            if (string.IsNullOrEmpty(displayPreferences.Id))
-            {
-                throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db => SaveDisplayPreferences(displayPreferences, userId, client, db),
-                    TransactionMode);
-            }
-        }
-
-        private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
-        {
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
-
-            using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
-            {
-                statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
-                statement.TryBind("@userId", userId.ToByteArray());
-                statement.TryBind("@client", client);
-                statement.TryBind("@data", serialized);
-
-                statement.MoveNext();
-            }
-        }
-
-        /// <summary>
-        /// Save all display preferences associated with a user in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        foreach (var displayPreference in displayPreferences)
-                        {
-                            SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
-                        }
-                    },
-                    TransactionMode);
-            }
-        }
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
-        {
-            if (string.IsNullOrEmpty(displayPreferencesId))
-            {
-                throw new ArgumentNullException(nameof(displayPreferencesId));
-            }
-
-            var guidId = displayPreferencesId.GetMD5();
-
-            using (var connection = GetConnection(true))
-            {
-                using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
-                {
-                    statement.TryBind("@id", guidId.ToByteArray());
-                    statement.TryBind("@userId", userId.ToByteArray());
-                    statement.TryBind("@client", client);
-
-                    foreach (var row in statement.ExecuteQuery())
-                    {
-                        return Get(row);
-                    }
-                }
-            }
-
-            return new DisplayPreferences
-            {
-                Id = guidId.ToString("N", CultureInfo.InvariantCulture)
-            };
-        }
-
-        /// <summary>
-        /// Gets all display preferences for the given user.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
-        {
-            var list = new List<DisplayPreferences>();
-
-            using (var connection = GetConnection(true))
-            using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
-            {
-                statement.TryBind("@userId", userId.ToByteArray());
-
-                foreach (var row in statement.ExecuteQuery())
-                {
-                    list.Add(Get(row));
-                }
-            }
-
-            return list;
-        }
-
-        private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
-            => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
-
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
-            => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
-
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
-            => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
-    }
-}

+ 21 - 113
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -9,6 +9,7 @@ using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
@@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
             "OwnerId"
             "OwnerId"
         };
         };
 
 
+        private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
         private static readonly string[] _mediaStreamSaveColumns =
         private static readonly string[] _mediaStreamSaveColumns =
         {
         {
             "ItemId",
             "ItemId",
@@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
             "ColorTransfer"
             "ColorTransfer"
         };
         };
 
 
+        private static readonly string _mediaStreamSaveColumnsInsertQuery =
+            $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+        private static readonly string _mediaStreamSaveColumnsSelectQuery =
+            $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
         private static readonly string[] _mediaAttachmentSaveColumns =
         private static readonly string[] _mediaAttachmentSaveColumns =
         {
         {
             "ItemId",
             "ItemId",
@@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
             "MIMEType"
             "MIMEType"
         };
         };
 
 
-        private static readonly string _mediaAttachmentInsertPrefix;
-
-        private static string GetSaveItemCommandText()
-        {
-            var saveColumns = new[]
-            {
-                "guid",
-                "type",
-                "data",
-                "Path",
-                "StartDate",
-                "EndDate",
-                "ChannelId",
-                "IsMovie",
-                "IsSeries",
-                "EpisodeTitle",
-                "IsRepeat",
-                "CommunityRating",
-                "CustomRating",
-                "IndexNumber",
-                "IsLocked",
-                "Name",
-                "OfficialRating",
-                "MediaType",
-                "Overview",
-                "ParentIndexNumber",
-                "PremiereDate",
-                "ProductionYear",
-                "ParentId",
-                "Genres",
-                "InheritedParentalRatingValue",
-                "SortName",
-                "ForcedSortName",
-                "RunTimeTicks",
-                "Size",
-                "DateCreated",
-                "DateModified",
-                "PreferredMetadataLanguage",
-                "PreferredMetadataCountryCode",
-                "Width",
-                "Height",
-                "DateLastRefreshed",
-                "DateLastSaved",
-                "IsInMixedFolder",
-                "LockedFields",
-                "Studios",
-                "Audio",
-                "ExternalServiceId",
-                "Tags",
-                "IsFolder",
-                "UnratedType",
-                "TopParentId",
-                "TrailerTypes",
-                "CriticRating",
-                "CleanName",
-                "PresentationUniqueKey",
-                "OriginalTitle",
-                "PrimaryVersionId",
-                "DateLastMediaAdded",
-                "Album",
-                "IsVirtualItem",
-                "SeriesName",
-                "UserDataKey",
-                "SeasonName",
-                "SeasonId",
-                "SeriesId",
-                "ExternalSeriesId",
-                "Tagline",
-                "ProviderIds",
-                "Images",
-                "ProductionLocations",
-                "ExtraIds",
-                "TotalBitrate",
-                "ExtraType",
-                "Artists",
-                "AlbumArtists",
-                "ExternalId",
-                "SeriesPresentationUniqueKey",
-                "ShowId",
-                "OwnerId"
-            };
-
-            var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
-
-            for (var i = 0; i < saveColumns.Length; i++)
-            {
-                if (i != 0)
-                {
-                    saveItemCommandCommandText += ",";
-                }
+        private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+            $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
 
 
-                saveItemCommandCommandText += "@" + saveColumns[i];
-            }
+        private static readonly string _mediaAttachmentInsertPrefix;
 
 
-            return saveItemCommandCommandText + ")";
-        }
+        private const string SaveItemCommandText =
+            @"replace into TypedBaseItems
+            (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+            values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
 
 
         /// <summary>
         /// <summary>
         /// Save a standard item in the repo.
         /// Save a standard item in the repo.
@@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
         {
         {
             var statements = PrepareAll(db, new string[]
             var statements = PrepareAll(db, new string[]
             {
             {
-                GetSaveItemCommandText(),
+                SaveItemCommandText,
                 "delete from AncestorIds where ItemId=@ItemId"
                 "delete from AncestorIds where ItemId=@ItemId"
             }).ToList();
             }).ToList();
 
 
@@ -1226,7 +1148,7 @@ namespace Emby.Server.Implementations.Data
 
 
             using (var connection = GetConnection(true))
             using (var connection = GetConnection(true))
             {
             {
-                using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
+                using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
                 {
                 {
                     statement.TryBind("@guid", id);
                     statement.TryBind("@guid", id);
 
 
@@ -5894,10 +5816,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 throw new ArgumentNullException(nameof(query));
                 throw new ArgumentNullException(nameof(query));
             }
             }
 
 
-            var cmdText = "select "
-                        + string.Join(",", _mediaStreamSaveColumns)
-                        + " from mediastreams where"
-                        + " ItemId=@ItemId";
+            var cmdText = _mediaStreamSaveColumnsSelectQuery;
 
 
             if (query.Type.HasValue)
             if (query.Type.HasValue)
             {
             {
@@ -5976,15 +5895,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
 
             while (startIndex < streams.Count)
             while (startIndex < streams.Count)
             {
             {
-                var insertText = new StringBuilder("insert into mediastreams (");
-                foreach (var column in _mediaStreamSaveColumns)
-                {
-                    insertText.Append(column).Append(',');
-                }
-
-                // Remove last comma
-                insertText.Length--;
-                insertText.Append(") values ");
+                var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
 
 
                 var endIndex = Math.Min(streams.Count, startIndex + Limit);
                 var endIndex = Math.Min(streams.Count, startIndex + Limit);
 
 
@@ -6251,10 +6162,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 throw new ArgumentNullException(nameof(query));
                 throw new ArgumentNullException(nameof(query));
             }
             }
 
 
-            var cmdText = "select "
-                        + string.Join(",", _mediaAttachmentSaveColumns)
-                        + " from mediaattachments where"
-                        + " ItemId=@ItemId";
+            var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
 
 
             if (query.Index.HasValue)
             if (query.Index.HasValue)
             {
             {

+ 12 - 11
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -5,8 +5,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Enums;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
@@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Caching.Memory;
 
 
 namespace Emby.Server.Implementations.Devices
 namespace Emby.Server.Implementations.Devices
 {
 {
     public class DeviceManager : IDeviceManager
     public class DeviceManager : IDeviceManager
     {
     {
+        private readonly IMemoryCache _memoryCache;
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IAuthenticationRepository _authRepo;
         private readonly IAuthenticationRepository _authRepo;
-        private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
         private readonly object _capabilitiesSyncLock = new object();
         private readonly object _capabilitiesSyncLock = new object();
 
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
             IAuthenticationRepository authRepo,
             IAuthenticationRepository authRepo,
             IJsonSerializer json,
             IJsonSerializer json,
             IUserManager userManager,
             IUserManager userManager,
-            IServerConfigurationManager config)
+            IServerConfigurationManager config,
+            IMemoryCache memoryCache)
         {
         {
             _json = json;
             _json = json;
             _userManager = userManager;
             _userManager = userManager;
             _config = config;
             _config = config;
+            _memoryCache = memoryCache;
             _authRepo = authRepo;
             _authRepo = authRepo;
-            _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
         public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
         public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
 
 
             lock (_capabilitiesSyncLock)
             lock (_capabilitiesSyncLock)
             {
             {
-                _capabilitiesCache[deviceId] = capabilities;
-
+                _memoryCache.CreateEntry(deviceId).SetValue(capabilities);
                 _json.SerializeToFile(capabilities, path);
                 _json.SerializeToFile(capabilities, path);
             }
             }
         }
         }
@@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
 
 
         public ClientCapabilities GetCapabilities(string id)
         public ClientCapabilities GetCapabilities(string id)
         {
         {
-            lock (_capabilitiesSyncLock)
+            if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
             {
             {
-                if (_capabilitiesCache.TryGetValue(id, out var result))
-                {
-                    return result;
-                }
+                return result;
+            }
 
 
+            lock (_capabilitiesSyncLock)
+            {
                 var path = Path.Combine(GetDevicePath(id), "capabilities.json");
                 var path = Path.Combine(GetDevicePath(id), "capabilities.json");
                 try
                 try
                 {
                 {

+ 2 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -25,7 +25,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
-    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
+    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@@ -54,7 +54,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

+ 1 - 1
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -3,7 +3,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 1
Emby.Server.Implementations/Images/FolderImageProvider.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 9 - 10
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1,7 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.MediaInfo;
 using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Person = MediaBrowser.Controller.Entities.Person;
 using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
@@ -63,6 +62,7 @@ namespace Emby.Server.Implementations.Library
         private const string ShortcutFileExtension = ".mblink";
         private const string ShortcutFileExtension = ".mblink";
 
 
         private readonly ILogger<LibraryManager> _logger;
         private readonly ILogger<LibraryManager> _logger;
+        private readonly IMemoryCache _memoryCache;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataRepository;
         private readonly IUserDataManager _userDataRepository;
@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IItemRepository _itemRepository;
         private readonly IItemRepository _itemRepository;
-        private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
 
 
         /// <summary>
         /// <summary>
@@ -112,6 +111,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="imageProcessor">The image processor.</param>
+        /// <param name="memoryCache">The memory cache.</param>
         public LibraryManager(
         public LibraryManager(
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ILogger<LibraryManager> logger,
             ILogger<LibraryManager> logger,
@@ -125,7 +125,8 @@ namespace Emby.Server.Implementations.Library
             Lazy<IUserViewManager> userviewManagerFactory,
             Lazy<IUserViewManager> userviewManagerFactory,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepository,
             IItemRepository itemRepository,
-            IImageProcessor imageProcessor)
+            IImageProcessor imageProcessor,
+            IMemoryCache memoryCache)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
             _logger = logger;
             _logger = logger;
@@ -140,8 +141,7 @@ namespace Emby.Server.Implementations.Library
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _itemRepository = itemRepository;
             _itemRepository = itemRepository;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
-
-            _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+            _memoryCache = memoryCache;
 
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
 
@@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
-            _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+            _memoryCache.CreateEntry(item.Id).SetValue(item);
         }
         }
 
 
         public void DeleteItem(BaseItem item, DeleteOptions options)
         public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -447,7 +447,7 @@ namespace Emby.Server.Implementations.Library
                 _itemRepository.DeleteItem(child.Id);
                 _itemRepository.DeleteItem(child.Id);
             }
             }
 
 
-            _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
+            _memoryCache.Remove(item.Id);
 
 
             ReportItemRemoved(item, parent);
             ReportItemRemoved(item, parent);
         }
         }
@@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("Guid can't be empty", nameof(id));
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
             }
 
 
-            if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
+            if (_memoryCache.TryGetValue(id, out BaseItem item))
             {
             {
                 return item;
                 return item;
             }
             }
@@ -1591,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         {
         {
             var tasks = IntroProviders
             var tasks = IntroProviders
-                .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
                 .Take(1)
                 .Take(1)
                 .Select(i => GetIntros(i, item, user));
                 .Select(i => GetIntros(i, item, user));
 
 

+ 1 - 1
Emby.Server.Implementations/Library/MusicManager.cs

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
 

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

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
 using MediaBrowser.Model.Search;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;

+ 1 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;

+ 2 - 2
Emby.Server.Implementations/Localization/Core/de.json

@@ -101,8 +101,8 @@
     "TaskCleanTranscode": "Lösche Transkodier Pfad",
     "TaskCleanTranscode": "Lösche Transkodier Pfad",
     "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
     "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
     "TaskUpdatePlugins": "Update Plugins",
     "TaskUpdatePlugins": "Update Plugins",
-    "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
-    "TaskRefreshPeople": "Erneuere Schausteller",
+    "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+    "TaskRefreshPeople": "Erneuere Schauspieler",
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",

+ 1 - 1
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
                 (octet[0] == 127) || // RFC1122
                 (octet[0] == 127) || // RFC1122
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
             {
             {
-                return false;
+                return true;
             }
             }
 
 
             if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
             if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))

+ 150 - 0
Jellyfin.Data/Entities/DisplayPreferences.cs

@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    /// <summary>
+    /// An entity representing a user's display preferences.
+    /// </summary>
+    public class DisplayPreferences
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+        /// </summary>
+        /// <param name="userId">The user's id.</param>
+        /// <param name="client">The client string.</param>
+        public DisplayPreferences(Guid userId, string client)
+        {
+            UserId = userId;
+            Client = client;
+            ShowSidebar = false;
+            ShowBackdrop = true;
+            SkipForwardLength = 30000;
+            SkipBackwardLength = 10000;
+            ScrollDirection = ScrollDirection.Horizontal;
+            ChromecastVersion = ChromecastVersion.Stable;
+            DashboardTheme = string.Empty;
+            TvHome = string.Empty;
+
+            HomeSections = new HashSet<HomeSection>();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+        /// </summary>
+        protected DisplayPreferences()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the user Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the client string.
+        /// </summary>
+        /// <remarks>
+        /// Required. Max Length = 32.
+        /// </remarks>
+        [Required]
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string Client { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to show the sidebar.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool ShowSidebar { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to show the backdrop.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool ShowBackdrop { get; set; }
+
+        /// <summary>
+        /// Gets or sets the scroll direction.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ScrollDirection ScrollDirection { get; set; }
+
+        /// <summary>
+        /// Gets or sets what the view should be indexed by.
+        /// </summary>
+        public IndexingKind? IndexBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the length of time to skip forwards, in milliseconds.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int SkipForwardLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the length of time to skip backwards, in milliseconds.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int SkipBackwardLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Chromecast Version.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ChromecastVersion ChromecastVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the next video info overlay should be shown.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool EnableNextVideoInfoOverlay { get; set; }
+
+        /// <summary>
+        /// Gets or sets the dashboard theme.
+        /// </summary>
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string DashboardTheme { get; set; }
+
+        /// <summary>
+        /// Gets or sets the tv home screen.
+        /// </summary>
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string TvHome { get; set; }
+
+        /// <summary>
+        /// Gets or sets the home sections.
+        /// </summary>
+        public virtual ICollection<HomeSection> HomeSections { get; protected set; }
+    }
+}

+ 46 - 0
Jellyfin.Data/Entities/HomeSection.cs

@@ -0,0 +1,46 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    /// <summary>
+    /// An entity representing a section on the user's home page.
+    /// </summary>
+    public class HomeSection
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <remarks>
+        /// Identity. Required.
+        /// </remarks>
+        [Key]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the Id of the associated display preferences.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int DisplayPreferencesId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the order.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int Order { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public HomeSectionType Type { get; set; }
+    }
+}

+ 120 - 0
Jellyfin.Data/Entities/ItemDisplayPreferences.cs

@@ -0,0 +1,120 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    public class ItemDisplayPreferences
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="client">The client.</param>
+        public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
+        {
+            UserId = userId;
+            ItemId = itemId;
+            Client = client;
+
+            SortBy = "SortName";
+            ViewType = ViewType.Poster;
+            SortOrder = SortOrder.Ascending;
+            RememberSorting = false;
+            RememberIndexing = false;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
+        /// </summary>
+        protected ItemDisplayPreferences()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the user Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id of the associated item.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public Guid ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the client string.
+        /// </summary>
+        /// <remarks>
+        /// Required. Max Length = 32.
+        /// </remarks>
+        [Required]
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string Client { get; set; }
+
+        /// <summary>
+        /// Gets or sets the view type.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ViewType ViewType { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the indexing should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool RememberIndexing { get; set; }
+
+        /// <summary>
+        /// Gets or sets what the view should be indexed by.
+        /// </summary>
+        public IndexingKind? IndexBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the sorting type should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool RememberSorting { get; set; }
+
+        /// <summary>
+        /// Gets or sets what the view should be sorted by.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        [MaxLength(64)]
+        [StringLength(64)]
+        public string SortBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sort order.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public SortOrder SortOrder { get; set; }
+    }
+}

+ 15 - 0
Jellyfin.Data/Entities/User.cs

@@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
             PasswordResetProviderId = passwordResetProviderId;
             PasswordResetProviderId = passwordResetProviderId;
 
 
             AccessSchedules = new HashSet<AccessSchedule>();
             AccessSchedules = new HashSet<AccessSchedule>();
+            ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
             // Groups = new HashSet<Group>();
             // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
             Permissions = new HashSet<Permission>();
             Preferences = new HashSet<Preference>();
             Preferences = new HashSet<Preference>();
@@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
         // [ForeignKey("UserId")]
         // [ForeignKey("UserId")]
         public virtual ImageInfo ProfileImage { get; set; }
         public virtual ImageInfo ProfileImage { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the user's display preferences.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public virtual DisplayPreferences DisplayPreferences { get; set; }
+
         [Required]
         [Required]
         public SyncPlayAccess SyncPlayAccess { get; set; }
         public SyncPlayAccess SyncPlayAccess { get; set; }
 
 
@@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// </summary>
         public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
         public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
 
 
+        /// <summary>
+        /// Gets or sets the list of item display preferences.
+        /// </summary>
+        public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
+
         /*
         /*
         /// <summary>
         /// <summary>
         /// Gets or sets the list of groups this user is a member of.
         /// Gets or sets the list of groups this user is a member of.

+ 18 - 0
Jellyfin.Data/Enums/ChromecastVersion.cs

@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the version of Chromecast to be used by clients.
+    /// </summary>
+    public enum ChromecastVersion
+    {
+        /// <summary>
+        /// Stable Chromecast version.
+        /// </summary>
+        Stable = 0,
+
+        /// <summary>
+        /// Unstable Chromecast version.
+        /// </summary>
+        Unstable = 1
+    }
+}

+ 53 - 0
Jellyfin.Data/Enums/HomeSectionType.cs

@@ -0,0 +1,53 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the different options for the home screen sections.
+    /// </summary>
+    public enum HomeSectionType
+    {
+        /// <summary>
+        /// None.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// My Media.
+        /// </summary>
+        SmallLibraryTiles = 1,
+
+        /// <summary>
+        /// My Media Small.
+        /// </summary>
+        LibraryButtons = 2,
+
+        /// <summary>
+        /// Active Recordings.
+        /// </summary>
+        ActiveRecordings = 3,
+
+        /// <summary>
+        /// Continue Watching.
+        /// </summary>
+        Resume = 4,
+
+        /// <summary>
+        /// Continue Listening.
+        /// </summary>
+        ResumeAudio = 5,
+
+        /// <summary>
+        /// Latest Media.
+        /// </summary>
+        LatestMedia = 6,
+
+        /// <summary>
+        /// Next Up.
+        /// </summary>
+        NextUp = 7,
+
+        /// <summary>
+        /// Live TV.
+        /// </summary>
+        LiveTv = 8
+    }
+}

+ 20 - 0
Jellyfin.Data/Enums/IndexingKind.cs

@@ -0,0 +1,20 @@
+namespace Jellyfin.Data.Enums
+{
+    public enum IndexingKind
+    {
+        /// <summary>
+        /// Index by the premiere date.
+        /// </summary>
+        PremiereDate = 0,
+
+        /// <summary>
+        /// Index by the production year.
+        /// </summary>
+        ProductionYear = 1,
+
+        /// <summary>
+        /// Index by the community rating.
+        /// </summary>
+        CommunityRating = 2
+    }
+}

+ 18 - 0
Jellyfin.Data/Enums/ScrollDirection.cs

@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the axis that should be scrolled.
+    /// </summary>
+    public enum ScrollDirection
+    {
+        /// <summary>
+        /// Horizontal scrolling direction.
+        /// </summary>
+        Horizontal = 0,
+
+        /// <summary>
+        /// Vertical scrolling direction.
+        /// </summary>
+        Vertical = 1
+    }
+}

+ 18 - 0
Jellyfin.Data/Enums/SortOrder.cs

@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the sorting order.
+    /// </summary>
+    public enum SortOrder
+    {
+        /// <summary>
+        /// Sort in increasing order.
+        /// </summary>
+        Ascending = 0,
+
+        /// <summary>
+        /// Sort in decreasing order.
+        /// </summary>
+        Descending = 1
+    }
+}

+ 38 - 0
Jellyfin.Data/Enums/ViewType.cs

@@ -0,0 +1,38 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the type of view for a library or collection.
+    /// </summary>
+    public enum ViewType
+    {
+        /// <summary>
+        /// Shows banners.
+        /// </summary>
+        Banner = 0,
+
+        /// <summary>
+        /// Shows a list of content.
+        /// </summary>
+        List = 1,
+
+        /// <summary>
+        /// Shows poster artwork.
+        /// </summary>
+        Poster = 2,
+
+        /// <summary>
+        /// Shows poster artwork with a card containing the name and year.
+        /// </summary>
+        PosterCard = 3,
+
+        /// <summary>
+        /// Shows a thumbnail.
+        /// </summary>
+        Thumb = 4,
+
+        /// <summary>
+        /// Shows a thumbnail with a card containing the name and year.
+        /// </summary>
+        ThumbCard = 5
+    }
+}

+ 4 - 5
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -18,11 +18,10 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="BlurHashSharp" Version="1.0.1" />
-    <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
-    <PackageReference Include="SkiaSharp" Version="1.68.3" />
-    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
-    <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
+    <PackageReference Include="BlurHashSharp" Version="1.1.0" />
+    <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
+    <PackageReference Include="SkiaSharp" Version="2.80.1" />
+    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 56 - 5
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -199,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
                 throw new ArgumentNullException(nameof(path));
                 throw new ArgumentNullException(nameof(path));
             }
             }
 
 
-            return BlurHashEncoder.Encode(xComp, yComp, path);
+            // Any larger than 128x128 is too slow and there's no visually discernible difference
+            return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
         }
         }
 
 
         private static bool HasDiacritics(string text)
         private static bool HasDiacritics(string text)
@@ -394,6 +395,56 @@ namespace Jellyfin.Drawing.Skia
             return rotated;
             return rotated;
         }
         }
 
 
+        /// <summary>
+        /// Resizes an image on the CPU, by utilizing a surface and canvas.
+        ///
+        /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
+        /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
+        /// </summary>
+        /// <param name="source">The source bitmap.</param>
+        /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
+        /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
+        /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
+        /// <returns>The resized image.</returns>
+        internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
+        {
+            using var surface = SKSurface.Create(targetInfo);
+            using var canvas = surface.Canvas;
+            using var paint = new SKPaint
+            {
+                FilterQuality = SKFilterQuality.High,
+                IsAntialias = isAntialias,
+                IsDither = isDither
+            };
+
+            var kernel = new float[9]
+            {
+                0,    -.1f,    0,
+                -.1f, 1.4f, -.1f,
+                0,    -.1f,    0,
+            };
+
+            var kernelSize = new SKSizeI(3, 3);
+            var kernelOffset = new SKPointI(1, 1);
+
+            paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
+                kernelSize,
+                kernel,
+                1f,
+                0f,
+                kernelOffset,
+                SKShaderTileMode.Clamp,
+                false);
+
+            canvas.DrawBitmap(
+                source,
+                SKRect.Create(0, 0, source.Width, source.Height),
+                SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+                paint);
+
+            return surface.Snapshot();
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
         public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
         {
         {
@@ -435,9 +486,9 @@ namespace Jellyfin.Drawing.Skia
             var width = newImageSize.Width;
             var width = newImageSize.Width;
             var height = newImageSize.Height;
             var height = newImageSize.Height;
 
 
-            using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType);
-            // scale image
-            bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+            // scale image (the FromImage creates a copy)
+            var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
+            using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
 
 
             // If all we're doing is resizing then we can stop now
             // If all we're doing is resizing then we can stop now
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
@@ -445,7 +496,7 @@ namespace Jellyfin.Drawing.Skia
                 Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                 Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                 using var outputStream = new SKFileWStream(outputPath);
                 using var outputStream = new SKFileWStream(outputPath);
                 using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
                 using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
-                pixmap.Encode(outputStream, skiaOutputFormat, quality);
+                resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
                 return outputPath;
                 return outputPath;
             }
             }
 
 

+ 6 - 8
Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -115,15 +115,13 @@ namespace Jellyfin.Drawing.Skia
 
 
                 // resize to the same aspect as the original
                 // resize to the same aspect as the original
                 int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
                 int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
-                using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
-                currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
+                using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
 
 
                 // crop image
                 // crop image
                 int ix = Math.Abs((iWidth - iSlice) / 2);
                 int ix = Math.Abs((iWidth - iSlice) / 2);
-                using var image = SKImage.FromBitmap(resizeBitmap);
-                using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
+                using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
                 // draw image onto canvas
                 // draw image onto canvas
-                canvas.DrawImage(subset ?? image, iSlice * i, 0);
+                canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
             }
             }
 
 
             return bitmap;
             return bitmap;
@@ -177,9 +175,9 @@ namespace Jellyfin.Drawing.Skia
                         continue;
                         continue;
                     }
                     }
 
 
-                    using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
-                    // scale image
-                    currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+                    // Scale image. The FromBitmap creates a copy
+                    var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
+                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo));
 
 
                     // draw this image into the strip at the next position
                     // draw this image into the strip at the next position
                     var xPos = x * cellWidth;
                     var xPos = x * cellWidth;

+ 4 - 0
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -28,8 +28,12 @@ namespace Jellyfin.Server.Implementations
 
 
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
 
 
+        public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
+
         public virtual DbSet<ImageInfo> ImageInfos { get; set; }
         public virtual DbSet<ImageInfo> ImageInfos { get; set; }
 
 
+        public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
+
         public virtual DbSet<Permission> Permissions { get; set; }
         public virtual DbSet<Permission> Permissions { get; set; }
 
 
         public virtual DbSet<Preference> Preferences { get; set; }
         public virtual DbSet<Preference> Preferences { get; set; }

+ 11 - 3
Jellyfin.Server.Implementations/JellyfinDbProvider.cs

@@ -1,4 +1,6 @@
 using System;
 using System;
+using System.IO;
+using MediaBrowser.Common.Configuration;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 
 
@@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations
     public class JellyfinDbProvider
     public class JellyfinDbProvider
     {
     {
         private readonly IServiceProvider _serviceProvider;
         private readonly IServiceProvider _serviceProvider;
+        private readonly IApplicationPaths _appPaths;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
         /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
         /// </summary>
         /// </summary>
         /// <param name="serviceProvider">The application's service provider.</param>
         /// <param name="serviceProvider">The application's service provider.</param>
-        public JellyfinDbProvider(IServiceProvider serviceProvider)
+        /// <param name="appPaths">The application paths.</param>
+        public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
         {
         {
             _serviceProvider = serviceProvider;
             _serviceProvider = serviceProvider;
-            serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
+            _appPaths = appPaths;
+
+            using var jellyfinDb = CreateContext();
+            jellyfinDb.Database.Migrate();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations
         /// <returns>The newly created context.</returns>
         /// <returns>The newly created context.</returns>
         public JellyfinDb CreateContext()
         public JellyfinDb CreateContext()
         {
         {
-            return _serviceProvider.GetRequiredService<JellyfinDb>();
+            var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
+            return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
         }
         }
     }
     }
 }
 }

+ 459 - 0
Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs

@@ -0,0 +1,459 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDb))]
+    [Migration("20200728005145_AddDisplayPreferences")]
+    partial class AddDisplayPreferences
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasDefaultSchema("jellyfin")
+                .HasAnnotation("ProductVersion", "3.1.6");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Permission_Permissions_Guid");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Preference_Preferences_Guid");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("Permission_Permissions_Guid");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("Preference_Preferences_Guid");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 132 - 0
Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs

@@ -0,0 +1,132 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class AddDisplayPreferences : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "DisplayPreferences",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: false),
+                    Client = table.Column<string>(maxLength: 32, nullable: false),
+                    ShowSidebar = table.Column<bool>(nullable: false),
+                    ShowBackdrop = table.Column<bool>(nullable: false),
+                    ScrollDirection = table.Column<int>(nullable: false),
+                    IndexBy = table.Column<int>(nullable: true),
+                    SkipForwardLength = table.Column<int>(nullable: false),
+                    SkipBackwardLength = table.Column<int>(nullable: false),
+                    ChromecastVersion = table.Column<int>(nullable: false),
+                    EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false),
+                    DashboardTheme = table.Column<string>(maxLength: 32, nullable: true),
+                    TvHome = table.Column<string>(maxLength: 32, nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_DisplayPreferences", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_DisplayPreferences_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "ItemDisplayPreferences",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: false),
+                    ItemId = table.Column<Guid>(nullable: false),
+                    Client = table.Column<string>(maxLength: 32, nullable: false),
+                    ViewType = table.Column<int>(nullable: false),
+                    RememberIndexing = table.Column<bool>(nullable: false),
+                    IndexBy = table.Column<int>(nullable: true),
+                    RememberSorting = table.Column<bool>(nullable: false),
+                    SortBy = table.Column<string>(maxLength: 64, nullable: false),
+                    SortOrder = table.Column<int>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_ItemDisplayPreferences_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "HomeSection",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    DisplayPreferencesId = table.Column<int>(nullable: false),
+                    Order = table.Column<int>(nullable: false),
+                    Type = table.Column<int>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_HomeSection", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
+                        column: x => x.DisplayPreferencesId,
+                        principalSchema: "jellyfin",
+                        principalTable: "DisplayPreferences",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences",
+                column: "UserId",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_HomeSection_DisplayPreferencesId",
+                schema: "jellyfin",
+                table: "HomeSection",
+                column: "DisplayPreferencesId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemDisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "ItemDisplayPreferences",
+                column: "UserId");
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "HomeSection",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "ItemDisplayPreferences",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "DisplayPreferences",
+                schema: "jellyfin");
+        }
+    }
+}

+ 148 - 1
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
 #pragma warning disable 612, 618
             modelBuilder
             modelBuilder
                 .HasDefaultSchema("jellyfin")
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.4");
+                .HasAnnotation("ProductVersion", "3.1.6");
 
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
                 {
@@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("ActivityLogs");
                     b.ToTable("ActivityLogs");
                 });
                 });
 
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
                 {
                 {
                     b.Property<int>("Id")
                     b.Property<int>("Id")
@@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("ImageInfos");
                     b.ToTable("ImageInfos");
                 });
                 });
 
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                 {
                     b.Property<int>("Id")
                     b.Property<int>("Id")
@@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsRequired();
                         .IsRequired();
                 });
                 });
 
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
                 {
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                     b.HasOne("Jellyfin.Data.Entities.User", null)
@@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
                         .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
                 });
                 });
 
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                     b.HasOne("Jellyfin.Data.Entities.User", null)

+ 4 - 10
Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
+using System.Text.Json;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -11,7 +12,6 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 using MediaBrowser.Model.Users;
 
 
 namespace Jellyfin.Server.Implementations.Users
 namespace Jellyfin.Server.Implementations.Users
@@ -23,7 +23,6 @@ namespace Jellyfin.Server.Implementations.Users
     {
     {
         private const string BaseResetFileName = "passwordreset";
         private const string BaseResetFileName = "passwordreset";
 
 
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
 
 
         private readonly string _passwordResetFileBase;
         private readonly string _passwordResetFileBase;
@@ -33,16 +32,11 @@ namespace Jellyfin.Server.Implementations.Users
         /// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
         /// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
         /// </summary>
         /// </summary>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
-        /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
-        public DefaultPasswordResetProvider(
-            IServerConfigurationManager configurationManager,
-            IJsonSerializer jsonSerializer,
-            IApplicationHost appHost)
+        public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost)
         {
         {
             _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
             _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
             _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
             _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
-            _jsonSerializer = jsonSerializer;
             _appHost = appHost;
             _appHost = appHost;
             // TODO: Remove the circular dependency on UserManager
             // TODO: Remove the circular dependency on UserManager
         }
         }
@@ -63,7 +57,7 @@ namespace Jellyfin.Server.Implementations.Users
                 SerializablePasswordReset spr;
                 SerializablePasswordReset spr;
                 await using (var str = File.OpenRead(resetFile))
                 await using (var str = File.OpenRead(resetFile))
                 {
                 {
-                    spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
+                    spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
                 }
                 }
 
 
                 if (spr.ExpirationDate < DateTime.UtcNow)
                 if (spr.ExpirationDate < DateTime.UtcNow)
@@ -119,7 +113,7 @@ namespace Jellyfin.Server.Implementations.Users
 
 
             await using (FileStream fileStream = File.OpenWrite(filePath))
             await using (FileStream fileStream = File.OpenWrite(filePath))
             {
             {
-                _jsonSerializer.SerializeToStream(spr, fileStream);
+                await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
                 await fileStream.FlushAsync().ConfigureAwait(false);
                 await fileStream.FlushAsync().ConfigureAwait(false);
             }
             }
 
 

+ 88 - 0
Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs

@@ -0,0 +1,88 @@
+#pragma warning disable CA1307
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+    /// <summary>
+    /// Manages the storage and retrieval of display preferences through Entity Framework.
+    /// </summary>
+    public class DisplayPreferencesManager : IDisplayPreferencesManager
+    {
+        private readonly JellyfinDbProvider _dbProvider;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
+        /// </summary>
+        /// <param name="dbProvider">The Jellyfin db provider.</param>
+        public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
+        {
+            _dbProvider = dbProvider;
+        }
+
+        /// <inheritdoc />
+        public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            var prefs = dbContext.DisplayPreferences
+                .Include(pref => pref.HomeSections)
+                .FirstOrDefault(pref =>
+                    pref.UserId == userId && string.Equals(pref.Client, client));
+
+            if (prefs == null)
+            {
+                prefs = new DisplayPreferences(userId, client);
+                dbContext.DisplayPreferences.Add(prefs);
+            }
+
+            return prefs;
+        }
+
+        /// <inheritdoc />
+        public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            var prefs = dbContext.ItemDisplayPreferences
+                .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
+
+            if (prefs == null)
+            {
+                prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
+                dbContext.ItemDisplayPreferences.Add(prefs);
+            }
+
+            return prefs;
+        }
+
+        /// <inheritdoc />
+        public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+
+            return dbContext.ItemDisplayPreferences
+                .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
+                .ToList();
+        }
+
+        /// <inheritdoc />
+        public void SaveChanges(DisplayPreferences preferences)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            dbContext.Update(preferences);
+            dbContext.SaveChanges();
+        }
+
+        /// <inheritdoc />
+        public void SaveChanges(ItemDisplayPreferences preferences)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            dbContext.Update(preferences);
+            dbContext.SaveChanges();
+        }
+    }
+}

+ 1 - 6
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -600,18 +600,13 @@ namespace Jellyfin.Server.Implementations.Users
             }
             }
 
 
             var defaultName = Environment.UserName;
             var defaultName = Environment.UserName;
-            if (string.IsNullOrWhiteSpace(defaultName))
+            if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
             {
             {
                 defaultName = "MyJellyfinUser";
                 defaultName = "MyJellyfinUser";
             }
             }
 
 
             _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
             _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 
 
-            if (!IsValidUsername(defaultName))
-            {
-                throw new ArgumentException("Provided username is not valid!", defaultName);
-            }
-
             var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
             var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
             newUser.SetPermission(PermissionKind.IsAdministrator, true);
             newUser.SetPermission(PermissionKind.IsAdministrator, true);
             newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
             newUser.SetPermission(PermissionKind.EnableContentDeletion, true);

+ 7 - 5
Jellyfin.Server/CoreAppHost.cs

@@ -64,16 +64,18 @@ namespace Jellyfin.Server
                 Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
                 Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
             }
             }
 
 
-            // TODO: Set up scoping and use AddDbContextPool
-            serviceCollection.AddDbContext<JellyfinDb>(
-                options => options
-                    .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
-                ServiceLifetime.Transient);
+            // TODO: Set up scoping and use AddDbContextPool,
+            // can't register as Transient since tracking transient in GC is funky
+            // serviceCollection.AddDbContext<JellyfinDb>(
+            //     options => options
+            //         .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
+            //     ServiceLifetime.Transient);
 
 
             serviceCollection.AddSingleton<JellyfinDbProvider>();
             serviceCollection.AddSingleton<JellyfinDbProvider>();
 
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
+            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 
 
             base.RegisterServices(serviceCollection);
             base.RegisterServices(serviceCollection);
         }
         }

+ 2 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.MigrateUserDb),
             typeof(Routines.MigrateUserDb),
-            typeof(Routines.ReaddDefaultPluginRepository)
+            typeof(Routines.ReaddDefaultPluginRepository),
+            typeof(Routines.MigrateDisplayPreferencesDb)
         };
         };
 
 
         /// <summary>
         /// <summary>

+ 174 - 0
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// The migration routine for migrating the display preferences database to EF Core.
+    /// </summary>
+    public class MigrateDisplayPreferencesDb : IMigrationRoutine
+    {
+        private const string DbFilename = "displaypreferences.db";
+
+        private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
+        private readonly IServerApplicationPaths _paths;
+        private readonly JellyfinDbProvider _provider;
+        private readonly JsonSerializerOptions _jsonOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="paths">The server application paths.</param>
+        /// <param name="provider">The database provider.</param>
+        public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+        {
+            _logger = logger;
+            _paths = paths;
+            _provider = provider;
+            _jsonOptions = new JsonSerializerOptions();
+            _jsonOptions.Converters.Add(new JsonStringEnumConverter());
+        }
+
+        /// <inheritdoc />
+        public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
+
+        /// <inheritdoc />
+        public string Name => "MigrateDisplayPreferencesDatabase";
+
+        /// <inheritdoc />
+        public bool PerformOnNewInstall => false;
+
+        /// <inheritdoc />
+        public void Perform()
+        {
+            HomeSectionType[] defaults =
+            {
+                HomeSectionType.SmallLibraryTiles,
+                HomeSectionType.Resume,
+                HomeSectionType.ResumeAudio,
+                HomeSectionType.LiveTv,
+                HomeSectionType.NextUp,
+                HomeSectionType.LatestMedia,
+                HomeSectionType.None,
+            };
+
+            var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase)
+            {
+                { "stable", ChromecastVersion.Stable },
+                { "nightly", ChromecastVersion.Unstable },
+                { "unstable", ChromecastVersion.Unstable }
+            };
+
+            var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
+            using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
+            {
+                using var dbContext = _provider.CreateContext();
+
+                var results = connection.Query("SELECT * FROM userdisplaypreferences");
+                foreach (var result in results)
+                {
+                    var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+                    var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
+                        ? chromecastDict[version]
+                        : ChromecastVersion.Stable;
+
+                    var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
+                    {
+                        IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
+                        ShowBackdrop = dto.ShowBackdrop,
+                        ShowSidebar = dto.ShowSidebar,
+                        ScrollDirection = dto.ScrollDirection,
+                        ChromecastVersion = chromecastVersion,
+                        SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
+                            ? int.Parse(length, CultureInfo.InvariantCulture)
+                            : 30000,
+                        SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
+                            ? int.Parse(length, CultureInfo.InvariantCulture)
+                            : 10000,
+                        EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
+                            ? bool.Parse(enabled)
+                            : true,
+                        DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
+                        TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
+                    };
+
+                    for (int i = 0; i < 7; i++)
+                    {
+                        dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
+
+                        displayPreferences.HomeSections.Add(new HomeSection
+                        {
+                            Order = i,
+                            Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
+                        });
+                    }
+
+                    var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
+                    {
+                        SortBy = dto.SortBy ?? "SortName",
+                        SortOrder = dto.SortOrder,
+                        RememberIndexing = dto.RememberIndexing,
+                        RememberSorting = dto.RememberSorting,
+                    };
+
+                    dbContext.Add(defaultLibraryPrefs);
+
+                    foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
+                    {
+                        if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))
+                        {
+                            continue;
+                        }
+
+                        var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
+                        {
+                            SortBy = dto.SortBy ?? "SortName",
+                            SortOrder = dto.SortOrder,
+                            RememberIndexing = dto.RememberIndexing,
+                            RememberSorting = dto.RememberSorting,
+                        };
+
+                        if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType))
+                        {
+                            libraryDisplayPreferences.ViewType = viewType;
+                        }
+
+                        dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
+                    }
+
+                    dbContext.Add(displayPreferences);
+                }
+
+                dbContext.SaveChanges();
+            }
+
+            try
+            {
+                File.Move(dbFilePath, dbFilePath + ".old");
+
+                var journalPath = dbFilePath + "-journal";
+                if (File.Exists(journalPath))
+                {
+                    File.Move(journalPath, dbFilePath + ".old-journal");
+                }
+            }
+            catch (IOException e)
+            {
+                _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'");
+            }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Api/ChannelService.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Api.UserLibrary;
 using MediaBrowser.Api.UserLibrary;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;

+ 108 - 20
MediaBrowser.Api/DisplayPreferencesService.cs

@@ -1,9 +1,11 @@
-using System.Threading;
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -13,7 +15,7 @@ namespace MediaBrowser.Api
     /// Class UpdateDisplayPreferences.
     /// Class UpdateDisplayPreferences.
     /// </summary>
     /// </summary>
     [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
     [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
-    public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
+    public class UpdateDisplayPreferences : DisplayPreferencesDto, IReturnVoid
     {
     {
         /// <summary>
         /// <summary>
         /// Gets or sets the id.
         /// Gets or sets the id.
@@ -27,7 +29,7 @@ namespace MediaBrowser.Api
     }
     }
 
 
     [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
     [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferences>
+    public class GetDisplayPreferences : IReturn<DisplayPreferencesDto>
     {
     {
         /// <summary>
         /// <summary>
         /// Gets or sets the id.
         /// Gets or sets the id.
@@ -50,28 +52,21 @@ namespace MediaBrowser.Api
     public class DisplayPreferencesService : BaseApiService
     public class DisplayPreferencesService : BaseApiService
     {
     {
         /// <summary>
         /// <summary>
-        /// The _display preferences manager.
+        /// The display preferences manager.
         /// </summary>
         /// </summary>
-        private readonly IDisplayPreferencesRepository _displayPreferencesManager;
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IDisplayPreferencesManager _displayPreferencesManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
         /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
         /// </summary>
         /// </summary>
-        /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="displayPreferencesManager">The display preferences manager.</param>
         /// <param name="displayPreferencesManager">The display preferences manager.</param>
         public DisplayPreferencesService(
         public DisplayPreferencesService(
             ILogger<DisplayPreferencesService> logger,
             ILogger<DisplayPreferencesService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IDisplayPreferencesRepository displayPreferencesManager)
+            IDisplayPreferencesManager displayPreferencesManager)
             : base(logger, serverConfigurationManager, httpResultFactory)
             : base(logger, serverConfigurationManager, httpResultFactory)
         {
         {
-            _jsonSerializer = jsonSerializer;
             _displayPreferencesManager = displayPreferencesManager;
             _displayPreferencesManager = displayPreferencesManager;
         }
         }
 
 
@@ -81,9 +76,41 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         public object Get(GetDisplayPreferences request)
         public object Get(GetDisplayPreferences request)
         {
         {
-            var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
+            var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
+            var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
+
+            var dto = new DisplayPreferencesDto
+            {
+                Client = displayPreferences.Client,
+                Id = displayPreferences.UserId.ToString(),
+                ViewType = itemPreferences.ViewType.ToString(),
+                SortBy = itemPreferences.SortBy,
+                SortOrder = itemPreferences.SortOrder,
+                IndexBy = displayPreferences.IndexBy?.ToString(),
+                RememberIndexing = itemPreferences.RememberIndexing,
+                RememberSorting = itemPreferences.RememberSorting,
+                ScrollDirection = displayPreferences.ScrollDirection,
+                ShowBackdrop = displayPreferences.ShowBackdrop,
+                ShowSidebar = displayPreferences.ShowSidebar
+            };
+
+            foreach (var homeSection in displayPreferences.HomeSections)
+            {
+                dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
+            }
 
 
-            return ToOptimizedResult(result);
+            foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
+            {
+                dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
+            }
+
+            dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
+            dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString();
+            dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString();
+            dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString();
+            dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+
+            return ToOptimizedResult(dto);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -92,10 +119,71 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         public void Post(UpdateDisplayPreferences request)
         public void Post(UpdateDisplayPreferences request)
         {
         {
-            // Serialize to json and then back so that the core doesn't see the request dto type
-            var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
+            HomeSectionType[] defaults =
+            {
+                HomeSectionType.SmallLibraryTiles,
+                HomeSectionType.Resume,
+                HomeSectionType.ResumeAudio,
+                HomeSectionType.LiveTv,
+                HomeSectionType.NextUp,
+                HomeSectionType.LatestMedia,
+                HomeSectionType.None,
+            };
+
+            var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
+
+            prefs.IndexBy = Enum.TryParse<IndexingKind>(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+            prefs.ShowBackdrop = request.ShowBackdrop;
+            prefs.ShowSidebar = request.ShowSidebar;
+
+            prefs.ScrollDirection = request.ScrollDirection;
+            prefs.ChromecastVersion = request.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+                ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
+                : ChromecastVersion.Stable;
+            prefs.EnableNextVideoInfoOverlay = request.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+                ? bool.Parse(enableNextVideoInfoOverlay)
+                : true;
+            prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
+            prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
+            prefs.DashboardTheme = request.CustomPrefs.TryGetValue("dashboardTheme", out var theme) ? theme : string.Empty;
+            prefs.TvHome = request.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty;
+            prefs.HomeSections.Clear();
+
+            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))
+            {
+                var order = int.Parse(key.AsSpan().Slice("homesection".Length));
+                if (!Enum.TryParse<HomeSectionType>(request.CustomPrefs[key], true, out var type))
+                {
+                    type = order < 7 ? defaults[order] : HomeSectionType.None;
+                }
+
+                prefs.HomeSections.Add(new HomeSection
+                {
+                    Order = order,
+                    Type = type
+                });
+            }
+
+            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("landing-")))
+            {
+                var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Parse(key.Substring("landing-".Length)), prefs.Client);
+                itemPreferences.ViewType = Enum.Parse<ViewType>(request.ViewType);
+                _displayPreferencesManager.SaveChanges(itemPreferences);
+            }
+
+            var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Empty, prefs.Client);
+            itemPrefs.SortBy = request.SortBy;
+            itemPrefs.SortOrder = request.SortOrder;
+            itemPrefs.RememberIndexing = request.RememberIndexing;
+            itemPrefs.RememberSorting = request.RememberSorting;
+
+            if (Enum.TryParse<ViewType>(request.ViewType, true, out var viewType))
+            {
+                itemPrefs.ViewType = viewType;
+            }
 
 
-            _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
+            _displayPreferencesManager.SaveChanges(prefs);
+            _displayPreferencesManager.SaveChanges(itemPrefs);
         }
         }
     }
     }
 }
 }

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 2 - 1
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -194,7 +194,8 @@ namespace MediaBrowser.Api.Playback.Hls
             var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
             var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
 
 
             // Main stream
             // Main stream
-            builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
+            builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
+                .AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
             var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
             var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
             builder.AppendLine(playlistUrl);
             builder.AppendLine(playlistUrl);
 
 

+ 9 - 5
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -968,7 +968,8 @@ namespace MediaBrowser.Api.Playback.Hls
             builder.AppendLine("#EXTM3U");
             builder.AppendLine("#EXTM3U");
             builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
             builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
             builder.AppendLine("#EXT-X-VERSION:3");
             builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
+            builder.Append("#EXT-X-TARGETDURATION:")
+                .AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
             builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
             builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
 
 
             var queryStringIndex = Request.RawUrl.IndexOf('?');
             var queryStringIndex = Request.RawUrl.IndexOf('?');
@@ -983,14 +984,17 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             foreach (var length in segmentLengths)
             foreach (var length in segmentLengths)
             {
             {
-                builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
-
-                builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
+                builder.Append("#EXTINF:")
+                    .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+                    .AppendLine(", nodesc");
 
 
+                builder.AppendFormat(
+                    CultureInfo.InvariantCulture,
+                    "hls1/{0}/{1}{2}{3}",
                     name,
                     name,
                     index.ToString(CultureInfo.InvariantCulture),
                     index.ToString(CultureInfo.InvariantCulture),
                     GetSegmentFileExtension(request),
                     GetSegmentFileExtension(request),
-                    queryString));
+                    queryString).AppendLine();
 
 
                 index++;
                 index++;
             }
             }

+ 9 - 6
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -175,11 +175,12 @@ namespace MediaBrowser.Api.Subtitles
                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
             }
             }
 
 
-            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");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+            builder.AppendLine("#EXTM3U")
+                .Append("#EXT-X-TARGETDURATION:")
+                .AppendLine(request.SegmentLength.ToString(CultureInfo.InvariantCulture))
+                .AppendLine("#EXT-X-VERSION:3")
+                .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
+                .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
 
 
             long positionTicks = 0;
             long positionTicks = 0;
 
 
@@ -190,7 +191,9 @@ namespace MediaBrowser.Api.Subtitles
                 var remaining = runtime - positionTicks;
                 var remaining = runtime - positionTicks;
                 var lengthTicks = Math.Min(remaining, segmentLengthTicks);
                 var lengthTicks = Math.Min(remaining, segmentLengthTicks);
 
 
-                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
+                builder.Append("#EXTINF:")
+                    .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
+                    .AppendLine(",");
 
 
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
 
 

+ 1 - 0
MediaBrowser.Api/SuggestionsService.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;

+ 1 - 0
MediaBrowser.Api/TvShowsService.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

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

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
@@ -466,8 +467,8 @@ namespace MediaBrowser.Api.UserLibrary
 
 
                 var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
                 var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
                 var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
                 var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
-                    ? MediaBrowser.Model.Entities.SortOrder.Descending
-                    : MediaBrowser.Model.Entities.SortOrder.Ascending;
+                    ? Jellyfin.Data.Enums.SortOrder.Descending
+                    : Jellyfin.Data.Enums.SortOrder.Ascending;
 
 
                 result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
                 result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
             }
             }

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;

+ 49 - 0
MediaBrowser.Controller/IDisplayPreferencesManager.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+
+namespace MediaBrowser.Controller
+{
+    /// <summary>
+    /// Manages the storage and retrieval of display preferences.
+    /// </summary>
+    public interface IDisplayPreferencesManager
+    {
+        /// <summary>
+        /// Gets the display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user's id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>The associated display preferences.</returns>
+        DisplayPreferences GetDisplayPreferences(Guid userId, string client);
+
+        /// <summary>
+        /// Gets the default item display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>The item display preferences.</returns>
+        ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client);
+
+        /// <summary>
+        /// Gets all of the item display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>A list of item display preferences.</returns>
+        IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
+
+        /// <summary>
+        /// Saves changes to the provided display preferences.
+        /// </summary>
+        /// <param name="preferences">The display preferences to save.</param>
+        void SaveChanges(DisplayPreferences preferences);
+
+        /// <summary>
+        /// Saves changes to the provided item display preferences.
+        /// </summary>
+        /// <param name="preferences">The item display preferences to save.</param>
+        void SaveChanges(ItemDisplayPreferences preferences);
+    }
+}

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;

+ 0 - 53
MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs

@@ -1,53 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
-    /// <summary>
-    /// Interface IDisplayPreferencesRepository.
-    /// </summary>
-    public interface IDisplayPreferencesRepository : IRepository
-    {
-        /// <summary>
-        /// Saves display preferences for an item.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        void SaveDisplayPreferences(
-            DisplayPreferences displayPreferences,
-            string userId,
-            string client,
-            CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Saves all display preferences for a user.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        void SaveAllDisplayPreferences(
-            IEnumerable<DisplayPreferences> displayPreferences,
-            Guid userId,
-            CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
-
-        /// <summary>
-        /// Gets all display preferences for the given user.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
-    }
-}

+ 1 - 0
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;

+ 8 - 3
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -1377,7 +1377,9 @@ namespace MediaBrowser.MediaEncoding.Probing
             // OR -> COMMENT. SUBTITLE: DESCRIPTION
             // OR -> COMMENT. SUBTITLE: DESCRIPTION
             // e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
             // e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
             // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
             // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
-            if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+            if (string.IsNullOrWhiteSpace(subTitle)
+                && !string.IsNullOrWhiteSpace(description)
+                && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
             {
             {
                 string[] parts = description.Split(':');
                 string[] parts = description.Split(':');
                 if (parts.Length > 0)
                 if (parts.Length > 0)
@@ -1385,7 +1387,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                     string subtitle = parts[0];
                     string subtitle = parts[0];
                     try
                     try
                     {
                     {
-                        if (subtitle.Contains("/")) // It contains a episode number and season number
+                        if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
                         {
                         {
                             string[] numbers = subtitle.Split(' ');
                             string[] numbers = subtitle.Split(' ');
                             video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
                             video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
@@ -1400,8 +1402,11 @@ namespace MediaBrowser.MediaEncoding.Probing
                     }
                     }
                     catch // Default parsing
                     catch // Default parsing
                     {
                     {
-                        if (subtitle.Contains(".")) // skip the comment, keep the subtitle
+                        if (subtitle.Contains('.', StringComparison.Ordinal))
+                        {
+                            // skip the comment, keep the subtitle
                             description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
                             description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+                        }
                         else
                         else
                         {
                         {
                             description = subtitle.Trim(); // Clean up whitespaces and save it
                             description = subtitle.Trim(); // Clean up whitespaces and save it

+ 6 - 6
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -731,19 +731,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
                 var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
                 var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
 
 
-                var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+                ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
 
 
-                var prefix = filename.Substring(0, 1);
+                var prefix = filename.Slice(0, 1);
 
 
-                return Path.Combine(SubtitleCachePath, prefix, filename);
+                return Path.Join(SubtitleCachePath, prefix, filename);
             }
             }
             else
             else
             {
             {
-                var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
+                ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
 
 
-                var prefix = filename.Substring(0, 1);
+                var prefix = filename.Slice(0, 1);
 
 
-                return Path.Combine(SubtitleCachePath, prefix, filename);
+                return Path.Join(SubtitleCachePath, prefix, filename);
             }
             }
         }
         }
 
 

+ 1 - 1
MediaBrowser.Model/Dlna/SortCriteria.cs

@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 
 namespace MediaBrowser.Model.Dlna
 namespace MediaBrowser.Model.Dlna
 {
 {

+ 4 - 8
MediaBrowser.Model/Entities/DisplayPreferences.cs → MediaBrowser.Model/Entities/DisplayPreferencesDto.cs

@@ -1,22 +1,18 @@
 #nullable disable
 #nullable disable
 using System.Collections.Generic;
 using System.Collections.Generic;
+using Jellyfin.Data.Enums;
 
 
 namespace MediaBrowser.Model.Entities
 namespace MediaBrowser.Model.Entities
 {
 {
     /// <summary>
     /// <summary>
     /// Defines the display preferences for any item that supports them (usually Folders).
     /// Defines the display preferences for any item that supports them (usually Folders).
     /// </summary>
     /// </summary>
-    public class DisplayPreferences
+    public class DisplayPreferencesDto
     {
     {
         /// <summary>
         /// <summary>
-        /// The image scale.
+        /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
         /// </summary>
         /// </summary>
-        private const double ImageScale = .9;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
-        /// </summary>
-        public DisplayPreferences()
+        public DisplayPreferencesDto()
         {
         {
             RememberIndexing = false;
             RememberIndexing = false;
             PrimaryImageHeight = 250;
             PrimaryImageHeight = 250;

+ 0 - 18
MediaBrowser.Model/Entities/ScrollDirection.cs

@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
-    /// <summary>
-    /// Enum ScrollDirection.
-    /// </summary>
-    public enum ScrollDirection
-    {
-        /// <summary>
-        /// The horizontal.
-        /// </summary>
-        Horizontal,
-
-        /// <summary>
-        /// The vertical.
-        /// </summary>
-        Vertical
-    }
-}

+ 0 - 18
MediaBrowser.Model/Entities/SortOrder.cs

@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
-    /// <summary>
-    /// Enum SortOrder.
-    /// </summary>
-    public enum SortOrder
-    {
-        /// <summary>
-        /// The ascending.
-        /// </summary>
-        Ascending,
-
-        /// <summary>
-        /// The descending.
-        /// </summary>
-        Descending
-    }
-}

+ 1 - 1
MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs

@@ -2,7 +2,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {

+ 1 - 1
MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs

@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 
 namespace MediaBrowser.Model.LiveTv
 namespace MediaBrowser.Model.LiveTv
 {
 {

+ 3 - 3
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
         private string GetAudioImagePath(Audio item)
         private string GetAudioImagePath(Audio item)
         {
         {
-            string filename = null;
+            string filename;
 
 
             if (item.GetType() == typeof(Audio))
             if (item.GetType() == typeof(Audio))
             {
             {
@@ -116,9 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo
                 filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg";
                 filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg";
             }
             }
 
 
-            var prefix = filename.Substring(0, 1);
+            var prefix = filename.AsSpan().Slice(0, 1);
 
 
-            return Path.Combine(AudioImagesPath, prefix, filename);
+            return Path.Join(AudioImagesPath, prefix, filename);
         }
         }
 
 
         public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
         public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");

+ 1 - 1
MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                         item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
                         item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
 
 
                         if (result.Year.Length > 0
                         if (result.Year.Length > 0
-                            && int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
+                            && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
                         {
                         {
                             item.ProductionYear = parsedYear;
                             item.ProductionYear = parsedYear;
                         }
                         }

+ 2 - 2
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             }
             }
 
 
             if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
             if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
-                && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+                && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
                 && year >= 0)
                 && year >= 0)
             {
             {
                 item.ProductionYear = year;
                 item.ProductionYear = year;
@@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             }
             }
 
 
             if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
             if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
-                && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+                && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
                 && year >= 0)
                 && year >= 0)
             {
             {
                 item.ProductionYear = year;
                 item.ProductionYear = year;

+ 2 - 2
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs

@@ -188,7 +188,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             for (var i = 0; i < episode.GuestStars.Length; ++i)
             for (var i = 0; i < episode.GuestStars.Length; ++i)
             {
             {
                 var currentActor = episode.GuestStars[i];
                 var currentActor = episode.GuestStars[i];
-                var roleStartIndex = currentActor.IndexOf('(');
+                var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
 
 
                 if (roleStartIndex == -1)
                 if (roleStartIndex == -1)
                 {
                 {
@@ -207,7 +207,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                 for (var j = i + 1; j < episode.GuestStars.Length; ++j)
                 for (var j = i + 1; j < episode.GuestStars.Length; ++j)
                 {
                 {
                     var currentRole = episode.GuestStars[j];
                     var currentRole = episode.GuestStars[j];
-                    var roleEndIndex = currentRole.IndexOf(')');
+                    var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
 
 
                     if (roleEndIndex == -1)
                     if (roleEndIndex == -1)
                     {
                     {

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs

@@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
 
 
         public BelongsToCollection Belongs_To_Collection { get; set; }
         public BelongsToCollection Belongs_To_Collection { get; set; }
 
 
-        public int Budget { get; set; }
+        public long Budget { get; set; }
 
 
         public List<Genre> Genres { get; set; }
         public List<Genre> Genres { get; set; }
 
 
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
 
 
         public string Release_Date { get; set; }
         public string Release_Date { get; set; }
 
 
-        public int Revenue { get; set; }
+        public long Revenue { get; set; }
 
 
         public int Runtime { get; set; }
         public int Runtime { get; set; }
 
 

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -251,9 +251,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 
 
         private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
         private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
         {
         {
-            var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
+            var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
 
 
-            return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
+            return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
         }
         }
 
 
         internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
         internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)

+ 16 - 6
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -222,8 +222,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
             if (index != -1)
             if (index != -1)
             {
             {
-                var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
-                if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+                var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+                index = tmdbId.IndexOf('-');
+                if (index != -1)
+                {
+                    tmdbId = tmdbId.Slice(0, index);
+                }
+
+                if (!tmdbId.IsEmpty
+                    && !tmdbId.IsWhiteSpace()
+                    && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
                 {
                 {
                     item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
                     item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
                 }
                 }
@@ -237,8 +245,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
                 if (index != -1)
                 if (index != -1)
                 {
                 {
-                    var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
-                    if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+                    var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+                    if (!tvdbId.IsEmpty
+                        && !tvdbId.IsWhiteSpace()
+                        && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
                     {
                     {
                         item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
                         item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
                     }
                     }
@@ -442,8 +452,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     {
                     {
                         var val = reader.ReadElementContentAsString();
                         var val = reader.ReadElementContentAsString();
 
 
-                        var hasAspectRatio = item as IHasAspectRatio;
-                        if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
+                        if (!string.IsNullOrWhiteSpace(val)
+                            && item is IHasAspectRatio hasAspectRatio)
                         {
                         {
                             hasAspectRatio.AspectRatio = val;
                             hasAspectRatio.AspectRatio = val;
                         }
                         }

+ 2 - 1
README.md

@@ -162,6 +162,7 @@ The following sections describe some more advanced scenarios for running the ser
 
 
 It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
 It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
 
 
-To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
+To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
+switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
 
 
 Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
 Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.

+ 2 - 2
SharedVersion.cs

@@ -1,4 +1,4 @@
 using System.Reflection;
 using System.Reflection;
 
 
-[assembly: AssemblyVersion("10.6.0")]
-[assembly: AssemblyFileVersion("10.6.0")]
+[assembly: AssemblyVersion("10.7.0")]
+[assembly: AssemblyFileVersion("10.7.0")]

+ 1 - 1
build.yaml

@@ -1,7 +1,7 @@
 ---
 ---
 # We just wrap `build` so this is really it
 # We just wrap `build` so this is really it
 name: "jellyfin"
 name: "jellyfin"
-version: "10.6.0"
+version: "10.7.0"
 packages:
 packages:
   - debian.amd64
   - debian.amd64
   - debian.arm64
   - debian.arm64

+ 2 - 1
bump_version

@@ -4,6 +4,7 @@
 
 
 set -o errexit
 set -o errexit
 set -o pipefail
 set -o pipefail
+set -o xtrace
 
 
 usage() {
 usage() {
     echo -e "bump_version - increase the shared version and generate changelogs"
     echo -e "bump_version - increase the shared version and generate changelogs"
@@ -58,7 +59,7 @@ sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
 debian_changelog_file="debian/changelog"
 debian_changelog_file="debian/changelog"
 debian_changelog_temp="$( mktemp )"
 debian_changelog_temp="$( mktemp )"
 # Create new temp file with our changelog
 # Create new temp file with our changelog
-echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
 
 
   * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
   * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
 
 

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+jellyfin-server (10.7.0-1) unstable; urgency=medium
+
+  * Forthcoming stable release
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  Mon, 27 Jul 2020 19:09:45 -0400
+
 jellyfin-server (10.6.0-2) unstable; urgency=medium
 jellyfin-server (10.6.0-2) unstable; urgency=medium
 
 
   * Fix upgrade bug
   * Fix upgrade bug

+ 1 - 1
debian/metapackage/jellyfin

@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
 Standards-Version: 3.9.2
 Standards-Version: 3.9.2
 
 
 Package: jellyfin
 Package: jellyfin
-Version: 10.6.0
+Version: 10.7.0
 Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
 Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
 Depends: jellyfin-server, jellyfin-web
 Depends: jellyfin-server, jellyfin-web
 Description: Provides the Jellyfin Free Software Media System
 Description: Provides the Jellyfin Free Software Media System

+ 3 - 1
fedora/jellyfin.spec

@@ -7,7 +7,7 @@
 %endif
 %endif
 
 
 Name:           jellyfin
 Name:           jellyfin
-Version:        10.6.0
+Version:        10.7.0
 Release:        1%{?dist}
 Release:        1%{?dist}
 Summary:        The Free Software Media System
 Summary:        The Free Software Media System
 License:        GPLv3
 License:        GPLv3
@@ -139,6 +139,8 @@ fi
 %systemd_postun_with_restart jellyfin.service
 %systemd_postun_with_restart jellyfin.service
 
 
 %changelog
 %changelog
+* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
+- Forthcoming stable release
 * Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
 * Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
 - Forthcoming stable release
 - Forthcoming stable release
 * Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
 * Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>