ソースを参照

Merge pull request #3578 from barronpm/displaypreferences-efcore

Migrate Display Preferences to EF Core
Bond-009 4 年 前
コミット
b7421db5fe
48 ファイル変更1688 行追加359 行削除
  1. 1 0
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  2. 0 3
      Emby.Server.Implementations/ApplicationHost.cs
  3. 1 0
      Emby.Server.Implementations/Channels/ChannelManager.cs
  4. 0 225
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  5. 1 0
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  6. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  7. 1 1
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  8. 1 1
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  9. 1 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  10. 0 1
      Emby.Server.Implementations/Library/LibraryManager.cs
  11. 1 1
      Emby.Server.Implementations/Library/MusicManager.cs
  12. 1 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  13. 1 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  14. 150 0
      Jellyfin.Data/Entities/DisplayPreferences.cs
  15. 46 0
      Jellyfin.Data/Entities/HomeSection.cs
  16. 120 0
      Jellyfin.Data/Entities/ItemDisplayPreferences.cs
  17. 15 0
      Jellyfin.Data/Entities/User.cs
  18. 18 0
      Jellyfin.Data/Enums/ChromecastVersion.cs
  19. 53 0
      Jellyfin.Data/Enums/HomeSectionType.cs
  20. 20 0
      Jellyfin.Data/Enums/IndexingKind.cs
  21. 18 0
      Jellyfin.Data/Enums/ScrollDirection.cs
  22. 18 0
      Jellyfin.Data/Enums/SortOrder.cs
  23. 38 0
      Jellyfin.Data/Enums/ViewType.cs
  24. 4 0
      Jellyfin.Server.Implementations/JellyfinDb.cs
  25. 459 0
      Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
  26. 132 0
      Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
  27. 148 1
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  28. 88 0
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  29. 1 0
      Jellyfin.Server/CoreAppHost.cs
  30. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  31. 174 0
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  32. 1 1
      MediaBrowser.Api/ChannelService.cs
  33. 108 20
      MediaBrowser.Api/DisplayPreferencesService.cs
  34. 1 0
      MediaBrowser.Api/Movies/MoviesService.cs
  35. 1 0
      MediaBrowser.Api/SuggestionsService.cs
  36. 1 0
      MediaBrowser.Api/TvShowsService.cs
  37. 3 2
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  38. 1 0
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  39. 49 0
      MediaBrowser.Controller/IDisplayPreferencesManager.cs
  40. 1 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  41. 0 53
      MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
  42. 1 0
      MediaBrowser.Controller/Playlists/Playlist.cs
  43. 1 1
      MediaBrowser.Model/Dlna/SortCriteria.cs
  44. 4 8
      MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
  45. 0 18
      MediaBrowser.Model/Entities/ScrollDirection.cs
  46. 0 18
      MediaBrowser.Model/Entities/SortOrder.cs
  47. 1 1
      MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
  48. 1 1
      MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs

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

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

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

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

+ 1 - 0
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;

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

+ 1 - 0
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -9,6 +9,7 @@ using System.Text;
 using System.Text.Json;
 using System.Threading;
 using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller;

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

@@ -54,7 +54,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

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

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

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

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

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

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

+ 0 - 1
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -50,7 +50,6 @@ using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
 namespace Emby.Server.Implementations.Library

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

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 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.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
 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.Xml;
 using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;

+ 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;
 
             AccessSchedules = new HashSet<AccessSchedule>();
+            ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
             // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
             Preferences = new HashSet<Preference>();
@@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
         // [ForeignKey("UserId")]
         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]
         public SyncPlayAccess SyncPlayAccess { get; set; }
 
@@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         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>
         /// 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 - 0
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -28,8 +28,12 @@ namespace Jellyfin.Server.Implementations
 
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
 
+        public virtual DbSet<DisplayPreferences> DisplayPreferences { 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<Preference> Preferences { get; set; }

+ 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
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.4");
+                .HasAnnotation("ProductVersion", "3.1.6");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
                     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")
@@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
                     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")
@@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .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)
@@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .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)

+ 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 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -75,6 +75,7 @@ namespace Jellyfin.Server
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
+            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 
             base.RegisterServices(serviceCollection);
         }

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

@@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.MigrateUserDb),
-            typeof(Routines.ReaddDefaultPluginRepository)
+            typeof(Routines.ReaddDefaultPluginRepository),
+            typeof(Routines.MigrateDisplayPreferencesDb)
         };
 
         /// <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.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Api.UserLibrary;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 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.Net;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 
@@ -13,7 +15,7 @@ namespace MediaBrowser.Api
     /// Class UpdateDisplayPreferences.
     /// </summary>
     [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>
         /// 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")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferences>
+    public class GetDisplayPreferences : IReturn<DisplayPreferencesDto>
     {
         /// <summary>
         /// Gets or sets the id.
@@ -50,28 +52,21 @@ namespace MediaBrowser.Api
     public class DisplayPreferencesService : BaseApiService
     {
         /// <summary>
-        /// The _display preferences manager.
+        /// The display preferences manager.
         /// </summary>
-        private readonly IDisplayPreferencesRepository _displayPreferencesManager;
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IDisplayPreferencesManager _displayPreferencesManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
         /// </summary>
-        /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="displayPreferencesManager">The display preferences manager.</param>
         public DisplayPreferencesService(
             ILogger<DisplayPreferencesService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IDisplayPreferencesRepository displayPreferencesManager)
+            IDisplayPreferencesManager displayPreferencesManager)
             : base(logger, serverConfigurationManager, httpResultFactory)
         {
-            _jsonSerializer = jsonSerializer;
             _displayPreferencesManager = displayPreferencesManager;
         }
 
@@ -81,9 +76,41 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         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>
@@ -92,10 +119,71 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         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.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
MediaBrowser.Api/SuggestionsService.cs

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

+ 1 - 0
MediaBrowser.Api/TvShowsService.cs

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

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

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
@@ -466,8 +467,8 @@ namespace MediaBrowser.Api.UserLibrary
 
                 var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
                 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);
             }

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 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.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 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.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;

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

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

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

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

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

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