Kaynağa Gözat

Migrate User DB to EF Core

Patrick Barron 5 yıl önce
ebeveyn
işleme
3eeb6576d8

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

@@ -562,7 +562,6 @@ namespace Emby.Server.Implementations
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
             serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
-            serviceCollection.AddSingleton<IUserManager, UserManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
             // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation

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

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

+ 18 - 17
Emby.Server.Implementations/Session/SessionManager.cs

@@ -5,6 +5,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
@@ -14,7 +15,6 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
@@ -27,6 +27,7 @@ using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 
 namespace Emby.Server.Implementations.Session
 {
@@ -254,7 +255,7 @@ namespace Emby.Server.Implementations.Session
             string deviceId,
             string deviceName,
             string remoteEndPoint,
-            Jellyfin.Data.Entities.User user)
+            User user)
         {
             CheckDisposed();
 
@@ -438,7 +439,7 @@ namespace Emby.Server.Implementations.Session
             string deviceId,
             string deviceName,
             string remoteEndPoint,
-            Jellyfin.Data.Entities.User user)
+            User user)
         {
             CheckDisposed();
 
@@ -457,7 +458,7 @@ namespace Emby.Server.Implementations.Session
 
             sessionInfo.UserId = user?.Id ?? Guid.Empty;
             sessionInfo.UserName = user?.Username;
-            sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user);
+            sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
             sessionInfo.RemoteEndPoint = remoteEndPoint;
             sessionInfo.Client = appName;
 
@@ -483,7 +484,7 @@ namespace Emby.Server.Implementations.Session
             string deviceId,
             string deviceName,
             string remoteEndPoint,
-            Jellyfin.Data.Entities.User user)
+            User user)
         {
             var sessionInfo = new SessionInfo(this, _logger)
             {
@@ -497,7 +498,7 @@ namespace Emby.Server.Implementations.Session
 
             sessionInfo.UserId = user?.Id ?? Guid.Empty;
             sessionInfo.UserName = username;
-            sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user);
+            sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
             sessionInfo.RemoteEndPoint = remoteEndPoint;
 
             if (string.IsNullOrEmpty(deviceName))
@@ -520,9 +521,9 @@ namespace Emby.Server.Implementations.Session
             return sessionInfo;
         }
 
-        private List<Jellyfin.Data.Entities.User> GetUsers(SessionInfo session)
+        private List<User> GetUsers(SessionInfo session)
         {
-            var users = new List<Jellyfin.Data.Entities.User>();
+            var users = new List<User>();
 
             if (session.UserId != Guid.Empty)
             {
@@ -680,7 +681,7 @@ namespace Emby.Server.Implementations.Session
         /// </summary>
         /// <param name="user">The user object.</param>
         /// <param name="item">The item.</param>
-        private void OnPlaybackStart(Jellyfin.Data.Entities.User user, BaseItem item)
+        private void OnPlaybackStart(User user, BaseItem item)
         {
             var data = _userDataManager.GetUserData(user, item);
 
@@ -763,7 +764,7 @@ namespace Emby.Server.Implementations.Session
             StartIdleCheckTimer();
         }
 
-        private void OnPlaybackProgress(Jellyfin.Data.Entities.User user, BaseItem item, PlaybackProgressInfo info)
+        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
         {
             var data = _userDataManager.GetUserData(user, item);
 
@@ -789,7 +790,7 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
-        private static bool UpdatePlaybackSettings(Jellyfin.Data.Entities.User user, PlaybackProgressInfo info, UserItemData data)
+        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
         {
             var changed = false;
 
@@ -949,7 +950,7 @@ namespace Emby.Server.Implementations.Session
                 _logger);
         }
 
-        private bool OnPlaybackStopped(Jellyfin.Data.Entities.User user, BaseItem item, long? positionTicks, bool playbackFailed)
+        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
         {
             bool playedToCompletion = false;
 
@@ -1163,7 +1164,7 @@ namespace Emby.Server.Implementations.Session
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
         }
 
-        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, Jellyfin.Data.Entities.User user)
+        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         {
             var item = _libraryManager.GetItemById(id);
 
@@ -1216,7 +1217,7 @@ namespace Emby.Server.Implementations.Session
             return new[] { item };
         }
 
-        private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, Jellyfin.Data.Entities.User user)
+        private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
         {
             var item = _libraryManager.GetItemById(id);
 
@@ -1399,7 +1400,7 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
 
-            Jellyfin.Data.Entities.User user = null;
+            User user = null;
             if (request.UserId != Guid.Empty)
             {
                 user = _userManager.GetUserById(request.UserId);
@@ -1455,7 +1456,7 @@ namespace Emby.Server.Implementations.Session
             return returnResult;
         }
 
-        private string GetAuthorizationToken(Jellyfin.Data.Entities.User user, string deviceId, string app, string appVersion, string deviceName)
+        private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
         {
             var existing = _authRepo.Get(
                 new AuthenticationInfoQuery
@@ -1701,7 +1702,7 @@ namespace Emby.Server.Implementations.Session
             return info;
         }
 
-        private string GetImageCacheTag(Jellyfin.Data.Entities.User user)
+        private string GetImageCacheTag(User user)
         {
             try
             {

+ 13 - 5
Jellyfin.Data/Entities/AccessSchedule.cs

@@ -1,5 +1,7 @@
-using System.ComponentModel.DataAnnotations;
+using System;
+using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
 using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
@@ -20,8 +22,9 @@ namespace Jellyfin.Data.Entities
         /// <param name="dayOfWeek">The day of the week.</param>
         /// <param name="startHour">The start hour.</param>
         /// <param name="endHour">The end hour.</param>
-        public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour)
+        public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
         {
+            UserId = userId;
             DayOfWeek = dayOfWeek;
             StartHour = startHour;
             EndHour = endHour;
@@ -34,15 +37,20 @@ namespace Jellyfin.Data.Entities
         /// <param name="startHour">The start hour.</param>
         /// <param name="endHour">The end hour.</param>
         /// <returns>The newly created instance.</returns>
-        public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour)
+        public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
         {
-            return new AccessSchedule(dayOfWeek, startHour, endHour);
+            return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
         }
 
+        [JsonIgnore]
         [Key]
         [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id { get; protected set; }
+        public int Id { get; set; }
+
+        [Required]
+        [ForeignKey("Id")]
+        public Guid UserId { get; set; }
 
         /// <summary>
         /// Gets or sets the day of week.

+ 17 - 24
Jellyfin.Data/Entities/Group.cs

@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities
 {
-    public partial class Group
+    public partial class Group : IHasPermissions, ISavingChanges
     {
         partial void Init();
 
@@ -14,35 +14,29 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         protected Group()
         {
-            GroupPermissions = new HashSet<Permission>();
+            Permissions = new HashSet<Permission>();
             ProviderMappings = new HashSet<ProviderMapping>();
             Preferences = new HashSet<Preference>();
 
             Init();
         }
 
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Group CreateGroupUnsafe()
-        {
-            return new Group();
-        }
-
         /// <summary>
         /// Public constructor with required data
         /// </summary>
         /// <param name="name"></param>
-        /// <param name="_user0"></param>
-        public Group(string name, User _user0)
+        /// <param name="user"></param>
+        public Group(string name, User user)
         {
-            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
-            this.Name = name;
+            if (string.IsNullOrEmpty(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
 
-            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
-            _user0.Groups.Add(this);
+            this.Name = name;
+            user.Groups.Add(this);
 
-            this.GroupPermissions = new HashSet<Permission>();
+            this.Permissions = new HashSet<Permission>();
             this.ProviderMappings = new HashSet<ProviderMapping>();
             this.Preferences = new HashSet<Preference>();
 
@@ -54,9 +48,9 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// <param name="name"></param>
         /// <param name="_user0"></param>
-        public static Group Create(string name, User _user0)
+        public static Group Create(string name, User user)
         {
-            return new Group(name, _user0);
+            return new Group(name, user);
         }
 
         /*************************************************************************
@@ -68,8 +62,7 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         [Key]
         [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id { get; protected set; }
+        public Guid Id { get; protected set; }
 
         /// <summary>
         /// Required, Max length = 255
@@ -96,13 +89,13 @@ namespace Jellyfin.Data.Entities
          *************************************************************************/
 
         [ForeignKey("Permission_GroupPermissions_Id")]
-        public virtual ICollection<Permission> GroupPermissions { get; protected set; }
+        public ICollection<Permission> Permissions { get; protected set; }
 
         [ForeignKey("ProviderMapping_ProviderMappings_Id")]
-        public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+        public ICollection<ProviderMapping> ProviderMappings { get; protected set; }
 
         [ForeignKey("Preference_Preferences_Id")]
-        public virtual ICollection<Preference> Preferences { get; protected set; }
+        public ICollection<Preference> Preferences { get; protected set; }
 
     }
 }

+ 18 - 35
Jellyfin.Data/Entities/Permission.cs

@@ -3,10 +3,11 @@ using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.Runtime.CompilerServices;
+using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
-    public partial class Permission
+    public partial class Permission : ISavingChanges
     {
         partial void Init();
 
@@ -18,33 +19,16 @@ namespace Jellyfin.Data.Entities
             Init();
         }
 
-        /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static Permission CreatePermissionUnsafe()
-        {
-            return new Permission();
-        }
-
         /// <summary>
         /// Public constructor with required data
         /// </summary>
         /// <param name="kind"></param>
         /// <param name="value"></param>
-        /// <param name="_user0"></param>
-        /// <param name="_group1"></param>
-        public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+        /// <param name="holderId"></param>
+        public Permission(PermissionKind kind, bool value)
         {
-            this.Kind = kind;
-
-            this.Value = value;
-
-            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
-            _user0.Permissions.Add(this);
-
-            if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
-            _group1.GroupPermissions.Add(this);
-
+            Kind = kind;
+            Value = value;
 
             Init();
         }
@@ -54,11 +38,10 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// <param name="kind"></param>
         /// <param name="value"></param>
-        /// <param name="_user0"></param>
-        /// <param name="_group1"></param>
-        public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+        /// <param name="holderId"></param>
+        public static Permission Create(PermissionKind kind, bool value)
         {
-            return new Permission(kind, value, _user0, _group1);
+            return new Permission(kind, value);
         }
 
         /*************************************************************************
@@ -76,31 +59,32 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Backing field for Kind
         /// </summary>
-        protected Enums.PermissionKind _Kind;
+        protected PermissionKind _Kind;
         /// <summary>
         /// When provided in a partial class, allows value of Kind to be changed before setting.
         /// </summary>
-        partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue);
+        partial void SetKind(PermissionKind oldValue, ref PermissionKind newValue);
         /// <summary>
         /// When provided in a partial class, allows value of Kind to be changed before returning.
         /// </summary>
-        partial void GetKind(ref Enums.PermissionKind result);
+        partial void GetKind(ref PermissionKind result);
 
         /// <summary>
         /// Required
         /// </summary>
         [Required]
-        public Enums.PermissionKind Kind
+        public PermissionKind Kind
         {
             get
             {
-                Enums.PermissionKind value = _Kind;
+                PermissionKind value = _Kind;
                 GetKind(ref value);
-                return (_Kind = value);
+                return _Kind = value;
             }
+
             set
             {
-                Enums.PermissionKind oldValue = _Kind;
+                PermissionKind oldValue = _Kind;
                 SetKind(oldValue, ref value);
                 if (oldValue != value)
                 {
@@ -117,7 +101,7 @@ namespace Jellyfin.Data.Entities
         public bool Value { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Required, ConcurrencyToken.
         /// </summary>
         [ConcurrencyCheck]
         [Required]
@@ -138,7 +122,6 @@ namespace Jellyfin.Data.Entities
         {
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }
-
     }
 }
 

+ 29 - 52
Jellyfin.Data/Entities/Preference.cs

@@ -1,63 +1,33 @@
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
-    public partial class Preference
+    /// <summary>
+    /// An entity representing a preference attached to a user or group.
+    /// </summary>
+    public class Preference : ISavingChanges
     {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Preference()
-        {
-            Init();
-        }
-
         /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// Initializes a new instance of the <see cref="Preference"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        public static Preference CreatePreferenceUnsafe()
+        /// <param name="kind">The preference kind.</param>
+        /// <param name="value">The value.</param>
+        public Preference(PreferenceKind kind, string value)
         {
-            return new Preference();
+            Kind = kind;
+            Value = value ?? throw new ArgumentNullException(nameof(value));
         }
 
         /// <summary>
-        /// Public constructor with required data
-        /// </summary>
-        /// <param name="kind"></param>
-        /// <param name="value"></param>
-        /// <param name="_user0"></param>
-        /// <param name="_group1"></param>
-        public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
-        {
-            this.Kind = kind;
-
-            if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
-            this.Value = value;
-
-            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
-            _user0.Preferences.Add(this);
-
-            if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
-            _group1.Preferences.Add(this);
-
-
-            Init();
-        }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
+        /// Initializes a new instance of the <see cref="Preference"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
-        /// <param name="kind"></param>
-        /// <param name="value"></param>
-        /// <param name="_user0"></param>
-        /// <param name="_group1"></param>
-        public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
+        protected Preference()
         {
-            return new Preference(kind, value, _user0, _group1);
         }
 
         /*************************************************************************
@@ -76,7 +46,7 @@ namespace Jellyfin.Data.Entities
         /// Required
         /// </summary>
         [Required]
-        public Enums.PreferenceKind Kind { get; set; }
+        public PreferenceKind Kind { get; set; }
 
         /// <summary>
         /// Required, Max length = 65535
@@ -87,21 +57,28 @@ namespace Jellyfin.Data.Entities
         public string Value { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Required, ConcurrencyToken.
         /// </summary>
         [ConcurrencyCheck]
         [Required]
         public uint RowVersion { get; set; }
 
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="kind">The preference kind.</param>
+        /// <param name="value">The value.</param>
+        /// <returns>The new instance.</returns>
+        public static Preference Create(PreferenceKind kind, string value)
+        {
+            return new Preference(kind, value);
+        }
+
+        /// <inheritdoc/>
         public void OnSavingChanges()
         {
             RowVersion++;
         }
-
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
     }
 }
 

+ 80 - 61
Jellyfin.Data/Entities/User.cs

@@ -9,45 +9,23 @@ using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
-    public partial class User
+    /// <summary>
+    /// An entity representing a user.
+    /// </summary>
+    public partial class User : IHasPermissions, ISavingChanges
     {
         /// <summary>
         /// The values being delimited here are Guids, so commas work as they do not appear in Guids.
         /// </summary>
         private const char Delimiter = ',';
 
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected User()
-        {
-            Groups = new HashSet<Group>();
-            Permissions = new HashSet<Permission>();
-            ProviderMappings = new HashSet<ProviderMapping>();
-            Preferences = new HashSet<Preference>();
-            AccessSchedules = new HashSet<AccessSchedule>();
-
-            Init();
-        }
-
         /// <summary>
-        /// Public constructor with required data
+        /// Initializes a new instance of the <see cref="User"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        /// <param name="username"></param>
-        /// <param name="mustUpdatePassword"></param>
-        /// <param name="authenticationProviderId"></param>
-        /// <param name="invalidLoginAttemptCount"></param>
-        /// <param name="subtitleMode"></param>
-        /// <param name="playDefaultAudioTrack"></param>
-        public User(
-            string username,
-            bool mustUpdatePassword,
-            string authenticationProviderId,
-            int invalidLoginAttemptCount,
-            SubtitlePlaybackMode subtitleMode,
-            bool playDefaultAudioTrack)
+        /// <param name="username">The username for the new user.</param>
+        /// <param name="authenticationProviderId">The authentication provider's Id</param>
+        public User(string username, string authenticationProviderId)
         {
             if (string.IsNullOrEmpty(username))
             {
@@ -60,11 +38,7 @@ namespace Jellyfin.Data.Entities
             }
 
             Username = username;
-            MustUpdatePassword = mustUpdatePassword;
             AuthenticationProviderId = authenticationProviderId;
-            InvalidLoginAttemptCount = invalidLoginAttemptCount;
-            SubtitleMode = subtitleMode;
-            PlayDefaultAudioTrack = playDefaultAudioTrack;
 
             Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
@@ -74,6 +48,8 @@ namespace Jellyfin.Data.Entities
 
             // Set default values
             Id = Guid.NewGuid();
+            InvalidLoginAttemptCount = 0;
+            MustUpdatePassword = false;
             DisplayMissingEpisodes = false;
             DisplayCollectionsView = false;
             HidePlayedInLatest = true;
@@ -81,36 +57,40 @@ namespace Jellyfin.Data.Entities
             RememberSubtitleSelections = true;
             EnableNextEpisodeAutoPlay = true;
             EnableAutoLogin = false;
+            PlayDefaultAudioTrack = true;
+            SubtitleMode = SubtitlePlaybackMode.Default;
 
+            AddDefaultPermissions();
+            AddDefaultPreferences();
             Init();
         }
 
         /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// Initializes a new instance of the <see cref="User"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
-        public static User CreateUserUnsafe()
+        protected User()
         {
-            return new User();
+            Groups = new HashSet<Group>();
+            Permissions = new HashSet<Permission>();
+            ProviderMappings = new HashSet<ProviderMapping>();
+            Preferences = new HashSet<Preference>();
+            AccessSchedules = new HashSet<AccessSchedule>();
+
+            AddDefaultPermissions();
+            AddDefaultPreferences();
+            Init();
         }
 
         /// <summary>
         /// Static create function (for use in LINQ queries, etc.)
         /// </summary>
-        /// <param name="username"></param>
-        /// <param name="mustUpdatePassword"></param>
-        /// <param name="authenticationProviderId"></param>
-        /// <param name="invalidLoginAttemptCount"></param>
-        /// <param name="subtitleMode"></param>
-        /// <param name="playDefaultAudioTrack"></param>
-        public static User Create(
-            string username,
-            bool mustUpdatePassword,
-            string authenticationProviderId,
-            int invalidLoginAttemptCount,
-            SubtitlePlaybackMode subtitleMode,
-            bool playDefaultAudioTrack)
+        /// <param name="username">The username for the created user.</param>
+        /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+        /// <returns>The created instance.</returns>
+        public static User Create(string username, string authenticationProviderId)
         {
-            return new User(username, mustUpdatePassword, authenticationProviderId, invalidLoginAttemptCount, subtitleMode, playDefaultAudioTrack);
+            return new User(username, authenticationProviderId);
         }
 
         /*************************************************************************
@@ -131,7 +111,6 @@ namespace Jellyfin.Data.Entities
         [Required]
         [MaxLength(255)]
         [StringLength(255)]
-        [JsonPropertyName("Name")]
         public string Username { get; set; }
 
         /// <summary>
@@ -199,6 +178,7 @@ namespace Jellyfin.Data.Entities
         public bool PlayDefaultAudioTrack { get; set; }
 
         /// <summary>
+        /// Gets or sets the subtitle language preference.
         /// Max length = 255
         /// </summary>
         [MaxLength(255)]
@@ -237,6 +217,7 @@ namespace Jellyfin.Data.Entities
         public int? RemoteClientBitrateLimit { get; set; }
 
         /// <summary>
+        /// Gets or sets the internal id.
         /// This is a temporary stopgap for until the library db is migrated.
         /// This corresponds to the value of the index of this user in the library db.
         /// </summary>
@@ -246,7 +227,8 @@ namespace Jellyfin.Data.Entities
         public ImageInfo ProfileImage { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
+        /// Required, ConcurrenyToken.
         /// </summary>
         [ConcurrencyCheck]
         [Required]
@@ -260,23 +242,25 @@ namespace Jellyfin.Data.Entities
         /*************************************************************************
          * Navigation properties
          *************************************************************************/
-        [ForeignKey("Group_Groups_Id")]
+        [ForeignKey("Group_Groups_Guid")]
         public ICollection<Group> Groups { get; protected set; }
 
-        [ForeignKey("Permission_Permissions_Id")]
+        [ForeignKey("Permission_Permissions_Guid")]
         public ICollection<Permission> Permissions { get; protected set; }
 
         [ForeignKey("ProviderMapping_ProviderMappings_Id")]
         public ICollection<ProviderMapping> ProviderMappings { get; protected set; }
 
-        [ForeignKey("Preference_Preferences_Id")]
+        [ForeignKey("Preference_Preferences_Guid")]
         public ICollection<Preference> Preferences { get; protected set; }
 
         public ICollection<AccessSchedule> AccessSchedules { get; protected set; }
 
+        partial void Init();
+
         public bool HasPermission(PermissionKind permission)
         {
-            return Permissions.Select(p => p.Kind).Contains(permission);
+            return Permissions.First(p => p.Kind == permission).Value;
         }
 
         public void SetPermission(PermissionKind kind, bool value)
@@ -287,11 +271,12 @@ namespace Jellyfin.Data.Entities
 
         public string[] GetPreference(PreferenceKind preference)
         {
-            return Preferences
+            var val = Preferences
                 .Where(p => p.Kind == preference)
                 .Select(p => p.Value)
-                .First()
-                .Split(Delimiter);
+                .First();
+
+            return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
         }
 
         public void SetPreference(PreferenceKind preference, string[] values)
@@ -332,5 +317,39 @@ namespace Jellyfin.Data.Entities
 
             return hour >= schedule.StartHour && hour <= schedule.EndHour;
         }
+
+        // TODO: make these user configurable?
+        private void AddDefaultPermissions()
+        {
+            Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
+            Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
+            Permissions.Add(new Permission(PermissionKind.IsHidden, false));
+            Permissions.Add(new Permission(PermissionKind.EnableAllChannels, false));
+            Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true));
+            Permissions.Add(new Permission(PermissionKind.EnableAllFolders, false));
+            Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false));
+            Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true));
+            Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true));
+            Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true));
+            Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true));
+            Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true));
+            Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true));
+            Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
+            Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
+            Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
+            Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
+            Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
+            Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
+            Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
+            Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
+        }
+
+        private void AddDefaultPreferences()
+        {
+            foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
+            {
+                Preferences.Add(new Preference(val, string.Empty));
+            }
+        }
     }
 }

+ 10 - 0
Jellyfin.Data/IHasPermissions.cs

@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+
+namespace Jellyfin.Data
+{
+    public interface IHasPermissions
+    {
+        ICollection<Permission> Permissions { get; }
+    }
+}

+ 7 - 3
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -16,6 +16,13 @@ namespace Jellyfin.Server.Implementations
     public partial class JellyfinDb : DbContext
     {
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+
+        public virtual DbSet<Group> Groups { get; set; }
+
+        public virtual DbSet<Permission> Permissions { get; set; }
+
+        public virtual DbSet<Preference> Preferences { get; set; }
+
         public virtual DbSet<Data.Entities.User> Users { get; set; }
         /*public virtual DbSet<Artwork> Artwork { get; set; }
         public virtual DbSet<Book> Books { get; set; }
@@ -30,7 +37,6 @@ namespace Jellyfin.Server.Implementations
         public virtual DbSet<Episode> Episodes { get; set; }
         public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
         public virtual DbSet<Genre> Genres { get; set; }
-        public virtual DbSet<Group> Groups { get; set; }
         public virtual DbSet<Library> Libraries { get; set; }
         public virtual DbSet<LibraryItem> LibraryItems { get; set; }
         public virtual DbSet<LibraryRoot> LibraryRoot { get; set; }
@@ -43,12 +49,10 @@ namespace Jellyfin.Server.Implementations
         public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
         public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
         public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
-        public virtual DbSet<Permission> Permissions { get; set; }
         public virtual DbSet<Person> People { get; set; }
         public virtual DbSet<PersonRole> PersonRoles { get; set; }
         public virtual DbSet<Photo> Photo { get; set; }
         public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
-        public virtual DbSet<Preference> Preferences { get; set; }
         public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
         public virtual DbSet<Rating> Ratings { get; set; }
 

+ 1 - 1
Jellyfin.Server.Implementations/JellyfinDbProvider.cs

@@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations
         public JellyfinDbProvider(IServiceProvider serviceProvider)
         {
             _serviceProvider = serviceProvider;
-            serviceProvider.GetService<JellyfinDb>().Database.Migrate();
+            serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
         }
 
         /// <summary>

+ 136 - 56
Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs → Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 #pragma warning disable SA1601
 
 // <auto-generated />
@@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace Jellyfin.Server.Implementations.Migrations
 {
     [DbContext(typeof(JellyfinDb))]
-    [Migration("20200504195702_UserSchema")]
-    partial class UserSchema
+    [Migration("20200517002411_AddUsers")]
+    partial class AddUsers
     {
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
         {
@@ -22,6 +22,31 @@ namespace Jellyfin.Server.Implementations.Migrations
                 .HasDefaultSchema("jellyfin")
                 .HasAnnotation("ProductVersion", "3.1.3");
 
+            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("AccessSchedule");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
                 {
                     b.Property<int>("Id")
@@ -65,17 +90,17 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("ActivityLog");
+                    b.ToTable("ActivityLogs");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
                 {
-                    b.Property<int>("Id")
+                    b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER");
+                        .HasColumnType("TEXT");
 
-                    b.Property<int?>("Group_Groups_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Group_Groups_Guid")
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("Name")
                         .IsRequired()
@@ -88,9 +113,27 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.HasIndex("Group_Groups_Id");
+                    b.HasIndex("Group_Groups_Guid");
+
+                    b.ToTable("Groups");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
 
-                    b.ToTable("Group");
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ImageInfo");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@@ -102,11 +145,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("Kind")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int?>("Permission_GroupPermissions_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Permission_GroupPermissions_Id")
+                        .HasColumnType("TEXT");
 
-                    b.Property<int?>("Permission_Permissions_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
@@ -119,9 +162,9 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("Permission_GroupPermissions_Id");
 
-                    b.HasIndex("Permission_Permissions_Id");
+                    b.HasIndex("Permission_Permissions_Guid");
 
-                    b.ToTable("Permission");
+                    b.ToTable("Permissions");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@@ -133,8 +176,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("Kind")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int?>("Preference_Preferences_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("Preference_Preferences_Id")
+                        .HasColumnType("TEXT");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
@@ -147,9 +193,11 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
+                    b.HasIndex("Preference_Preferences_Guid");
+
                     b.HasIndex("Preference_Preferences_Id");
 
-                    b.ToTable("Preference");
+                    b.ToTable("Preferences");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b =>
@@ -163,8 +211,8 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasColumnType("TEXT")
                         .HasMaxLength(65535);
 
-                    b.Property<int?>("ProviderMapping_ProviderMappings_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("ProviderMapping_ProviderMappings_Id")
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("ProviderName")
                         .IsRequired()
@@ -189,12 +237,11 @@ namespace Jellyfin.Server.Implementations.Migrations
 
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
                 {
-                    b.Property<int>("Id")
+                    b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER");
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("AudioLanguagePreference")
-                        .IsRequired()
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
@@ -203,71 +250,86 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
-                    b.Property<bool?>("DisplayCollectionsView")
+                    b.Property<bool>("DisplayCollectionsView")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("DisplayMissingEpisodes")
+                    b.Property<bool>("DisplayMissingEpisodes")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("EnableNextEpisodeAutoPlay")
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("EnableUserPreferenceAccess")
+                    b.Property<bool>("EnableLocalPassword")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("GroupedFolders")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("HidePlayedInLatest")
+                    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<string>("LatestItemExcludes")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<DateTime>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("LastLoginDate")
+                        .HasColumnType("TEXT");
 
                     b.Property<int?>("LoginAttemptsBeforeLockout")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool>("MustUpdatePassword")
+                    b.Property<int?>("MaxParentalAgeRating")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("MyMediaExcludes")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
 
-                    b.Property<string>("OrderedViews")
+                    b.Property<string>("Password")
                         .HasColumnType("TEXT")
                         .HasMaxLength(65535);
 
-                    b.Property<string>("Password")
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
                         .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                        .HasMaxLength(255);
 
                     b.Property<bool>("PlayDefaultAudioTrack")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("RememberAudioSelections")
+                    b.Property<int?>("ProfileImageId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("RememberSubtitleSelections")
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
                         .HasColumnType("INTEGER");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("SubtitleLanguagePrefernce")
+                    b.Property<string>("SubtitleLanguagePreference")
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
-                    b.Property<string>("SubtitleMode")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(255);
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
 
                     b.Property<string>("Username")
                         .IsRequired()
@@ -276,34 +338,45 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("User");
+                    b.HasIndex("ProfileImageId");
+
+                    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.Group", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Groups")
-                        .HasForeignKey("Group_Groups_Id");
+                        .HasForeignKey("Group_Groups_Guid");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.Group", null)
-                        .WithMany("GroupPermissions")
+                        .WithMany("Permissions")
                         .HasForeignKey("Permission_GroupPermissions_Id");
 
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Permissions")
-                        .HasForeignKey("Permission_Permissions_Id");
+                        .HasForeignKey("Permission_Permissions_Guid");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.Group", null)
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Preferences")
-                        .HasForeignKey("Preference_Preferences_Id");
+                        .HasForeignKey("Preference_Preferences_Guid");
 
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Data.Entities.Group", null)
                         .WithMany("Preferences")
                         .HasForeignKey("Preference_Preferences_Id");
                 });
@@ -318,6 +391,13 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .WithMany("ProviderMappings")
                         .HasForeignKey("ProviderMapping_ProviderMappings_Id");
                 });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage")
+                        .WithMany()
+                        .HasForeignKey("ProfileImageId");
+                });
 #pragma warning restore 612, 618
         }
     }

+ 138 - 60
Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs → Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs

@@ -1,74 +1,125 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 #pragma warning disable SA1601
 
+using System;
 using Microsoft.EntityFrameworkCore.Migrations;
 
 namespace Jellyfin.Server.Implementations.Migrations
 {
-    public partial class UserSchema : Migration
+    public partial class AddUsers : Migration
     {
         protected override void Up(MigrationBuilder migrationBuilder)
         {
             migrationBuilder.CreateTable(
-                name: "User",
+                name: "ImageInfo",
                 schema: "jellyfin",
                 columns: table => new
                 {
                     Id = table.Column<int>(nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
+                    Path = table.Column<string>(nullable: false),
+                    LastModified = table.Column<DateTime>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ImageInfo", x => x.Id);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Users",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(nullable: false),
                     Username = table.Column<string>(maxLength: 255, nullable: false),
                     Password = table.Column<string>(maxLength: 65535, nullable: true),
+                    EasyPassword = table.Column<string>(maxLength: 65535, nullable: true),
                     MustUpdatePassword = table.Column<bool>(nullable: false),
-                    AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: false),
+                    AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
                     AuthenticationProviderId = table.Column<string>(maxLength: 255, nullable: false),
-                    GroupedFolders = table.Column<string>(maxLength: 65535, nullable: true),
+                    PasswordResetProviderId = table.Column<string>(maxLength: 255, nullable: false),
                     InvalidLoginAttemptCount = table.Column<int>(nullable: false),
-                    LatestItemExcludes = table.Column<string>(maxLength: 65535, nullable: true),
+                    LastActivityDate = table.Column<DateTime>(nullable: false),
+                    LastLoginDate = table.Column<DateTime>(nullable: false),
                     LoginAttemptsBeforeLockout = table.Column<int>(nullable: true),
-                    MyMediaExcludes = table.Column<string>(maxLength: 65535, nullable: true),
-                    OrderedViews = table.Column<string>(maxLength: 65535, nullable: true),
-                    SubtitleMode = table.Column<string>(maxLength: 255, nullable: false),
+                    SubtitleMode = table.Column<int>(nullable: false),
                     PlayDefaultAudioTrack = table.Column<bool>(nullable: false),
-                    SubtitleLanguagePrefernce = table.Column<string>(maxLength: 255, nullable: true),
-                    DisplayMissingEpisodes = table.Column<bool>(nullable: true),
-                    DisplayCollectionsView = table.Column<bool>(nullable: true),
-                    HidePlayedInLatest = table.Column<bool>(nullable: true),
-                    RememberAudioSelections = table.Column<bool>(nullable: true),
-                    RememberSubtitleSelections = table.Column<bool>(nullable: true),
-                    EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: true),
-                    EnableUserPreferenceAccess = table.Column<bool>(nullable: true),
+                    SubtitleLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
+                    DisplayMissingEpisodes = table.Column<bool>(nullable: false),
+                    DisplayCollectionsView = table.Column<bool>(nullable: false),
+                    EnableLocalPassword = table.Column<bool>(nullable: false),
+                    HidePlayedInLatest = table.Column<bool>(nullable: false),
+                    RememberAudioSelections = table.Column<bool>(nullable: false),
+                    RememberSubtitleSelections = table.Column<bool>(nullable: false),
+                    EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: false),
+                    EnableAutoLogin = table.Column<bool>(nullable: false),
+                    EnableUserPreferenceAccess = table.Column<bool>(nullable: false),
+                    MaxParentalAgeRating = table.Column<int>(nullable: true),
+                    RemoteClientBitrateLimit = table.Column<int>(nullable: true),
+                    InternalId = table.Column<long>(nullable: false),
+                    ProfileImageId = table.Column<int>(nullable: true),
                     RowVersion = table.Column<uint>(nullable: false)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_User", x => x.Id);
+                    table.PrimaryKey("PK_Users", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_Users_ImageInfo_ProfileImageId",
+                        column: x => x.ProfileImageId,
+                        principalSchema: "jellyfin",
+                        principalTable: "ImageInfo",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Restrict);
                 });
 
             migrationBuilder.CreateTable(
-                name: "Group",
+                name: "AccessSchedule",
                 schema: "jellyfin",
                 columns: table => new
                 {
                     Id = table.Column<int>(nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: false),
+                    DayOfWeek = table.Column<int>(nullable: false),
+                    StartHour = table.Column<double>(nullable: false),
+                    EndHour = table.Column<double>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_AccessSchedule", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_AccessSchedule_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Groups",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(nullable: false),
                     Name = table.Column<string>(maxLength: 255, nullable: false),
                     RowVersion = table.Column<uint>(nullable: false),
-                    Group_Groups_Id = table.Column<int>(nullable: true)
+                    Group_Groups_Guid = table.Column<Guid>(nullable: true)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_Group", x => x.Id);
+                    table.PrimaryKey("PK_Groups", x => x.Id);
                     table.ForeignKey(
-                        name: "FK_Group_User_Group_Groups_Id",
-                        column: x => x.Group_Groups_Id,
+                        name: "FK_Groups_Users_Group_Groups_Guid",
+                        column: x => x.Group_Groups_Guid,
                         principalSchema: "jellyfin",
-                        principalTable: "User",
+                        principalTable: "Users",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                 });
 
             migrationBuilder.CreateTable(
-                name: "Permission",
+                name: "Permissions",
                 schema: "jellyfin",
                 columns: table => new
                 {
@@ -77,30 +128,30 @@ namespace Jellyfin.Server.Implementations.Migrations
                     Kind = table.Column<int>(nullable: false),
                     Value = table.Column<bool>(nullable: false),
                     RowVersion = table.Column<uint>(nullable: false),
-                    Permission_GroupPermissions_Id = table.Column<int>(nullable: true),
-                    Permission_Permissions_Id = table.Column<int>(nullable: true)
+                    Permission_GroupPermissions_Id = table.Column<Guid>(nullable: true),
+                    Permission_Permissions_Guid = table.Column<Guid>(nullable: true)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_Permission", x => x.Id);
+                    table.PrimaryKey("PK_Permissions", x => x.Id);
                     table.ForeignKey(
-                        name: "FK_Permission_Group_Permission_GroupPermissions_Id",
+                        name: "FK_Permissions_Groups_Permission_GroupPermissions_Id",
                         column: x => x.Permission_GroupPermissions_Id,
                         principalSchema: "jellyfin",
-                        principalTable: "Group",
+                        principalTable: "Groups",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                     table.ForeignKey(
-                        name: "FK_Permission_User_Permission_Permissions_Id",
-                        column: x => x.Permission_Permissions_Id,
+                        name: "FK_Permissions_Users_Permission_Permissions_Guid",
+                        column: x => x.Permission_Permissions_Guid,
                         principalSchema: "jellyfin",
-                        principalTable: "User",
+                        principalTable: "Users",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                 });
 
             migrationBuilder.CreateTable(
-                name: "Preference",
+                name: "Preferences",
                 schema: "jellyfin",
                 columns: table => new
                 {
@@ -109,23 +160,24 @@ namespace Jellyfin.Server.Implementations.Migrations
                     Kind = table.Column<int>(nullable: false),
                     Value = table.Column<string>(maxLength: 65535, nullable: false),
                     RowVersion = table.Column<uint>(nullable: false),
-                    Preference_Preferences_Id = table.Column<int>(nullable: true)
+                    Preference_Preferences_Guid = table.Column<Guid>(nullable: true),
+                    Preference_Preferences_Id = table.Column<Guid>(nullable: true)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_Preference", x => x.Id);
+                    table.PrimaryKey("PK_Preferences", x => x.Id);
                     table.ForeignKey(
-                        name: "FK_Preference_Group_Preference_Preferences_Id",
-                        column: x => x.Preference_Preferences_Id,
+                        name: "FK_Preferences_Users_Preference_Preferences_Guid",
+                        column: x => x.Preference_Preferences_Guid,
                         principalSchema: "jellyfin",
-                        principalTable: "Group",
+                        principalTable: "Users",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                     table.ForeignKey(
-                        name: "FK_Preference_User_Preference_Preferences_Id",
+                        name: "FK_Preferences_Groups_Preference_Preferences_Id",
                         column: x => x.Preference_Preferences_Id,
                         principalSchema: "jellyfin",
-                        principalTable: "User",
+                        principalTable: "Groups",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                 });
@@ -141,49 +193,61 @@ namespace Jellyfin.Server.Implementations.Migrations
                     ProviderSecrets = table.Column<string>(maxLength: 65535, nullable: false),
                     ProviderData = table.Column<string>(maxLength: 65535, nullable: false),
                     RowVersion = table.Column<uint>(nullable: false),
-                    ProviderMapping_ProviderMappings_Id = table.Column<int>(nullable: true)
+                    ProviderMapping_ProviderMappings_Id = table.Column<Guid>(nullable: true)
                 },
                 constraints: table =>
                 {
                     table.PrimaryKey("PK_ProviderMapping", x => x.Id);
                     table.ForeignKey(
-                        name: "FK_ProviderMapping_Group_ProviderMapping_ProviderMappings_Id",
+                        name: "FK_ProviderMapping_Groups_ProviderMapping_ProviderMappings_Id",
                         column: x => x.ProviderMapping_ProviderMappings_Id,
                         principalSchema: "jellyfin",
-                        principalTable: "Group",
+                        principalTable: "Groups",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                     table.ForeignKey(
-                        name: "FK_ProviderMapping_User_ProviderMapping_ProviderMappings_Id",
+                        name: "FK_ProviderMapping_Users_ProviderMapping_ProviderMappings_Id",
                         column: x => x.ProviderMapping_ProviderMappings_Id,
                         principalSchema: "jellyfin",
-                        principalTable: "User",
+                        principalTable: "Users",
                         principalColumn: "Id",
                         onDelete: ReferentialAction.Restrict);
                 });
 
             migrationBuilder.CreateIndex(
-                name: "IX_Group_Group_Groups_Id",
+                name: "IX_AccessSchedule_UserId",
                 schema: "jellyfin",
-                table: "Group",
-                column: "Group_Groups_Id");
+                table: "AccessSchedule",
+                column: "UserId");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Permission_Permission_GroupPermissions_Id",
+                name: "IX_Groups_Group_Groups_Guid",
                 schema: "jellyfin",
-                table: "Permission",
+                table: "Groups",
+                column: "Group_Groups_Guid");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Permissions_Permission_GroupPermissions_Id",
+                schema: "jellyfin",
+                table: "Permissions",
                 column: "Permission_GroupPermissions_Id");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Permission_Permission_Permissions_Id",
+                name: "IX_Permissions_Permission_Permissions_Guid",
+                schema: "jellyfin",
+                table: "Permissions",
+                column: "Permission_Permissions_Guid");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Preferences_Preference_Preferences_Guid",
                 schema: "jellyfin",
-                table: "Permission",
-                column: "Permission_Permissions_Id");
+                table: "Preferences",
+                column: "Preference_Preferences_Guid");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Preference_Preference_Preferences_Id",
+                name: "IX_Preferences_Preference_Preferences_Id",
                 schema: "jellyfin",
-                table: "Preference",
+                table: "Preferences",
                 column: "Preference_Preferences_Id");
 
             migrationBuilder.CreateIndex(
@@ -191,16 +255,26 @@ namespace Jellyfin.Server.Implementations.Migrations
                 schema: "jellyfin",
                 table: "ProviderMapping",
                 column: "ProviderMapping_ProviderMappings_Id");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Users_ProfileImageId",
+                schema: "jellyfin",
+                table: "Users",
+                column: "ProfileImageId");
         }
 
         protected override void Down(MigrationBuilder migrationBuilder)
         {
             migrationBuilder.DropTable(
-                name: "Permission",
+                name: "AccessSchedule",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "Permissions",
                 schema: "jellyfin");
 
             migrationBuilder.DropTable(
-                name: "Preference",
+                name: "Preferences",
                 schema: "jellyfin");
 
             migrationBuilder.DropTable(
@@ -208,11 +282,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                 schema: "jellyfin");
 
             migrationBuilder.DropTable(
-                name: "Group",
+                name: "Groups",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "Users",
                 schema: "jellyfin");
 
             migrationBuilder.DropTable(
-                name: "User",
+                name: "ImageInfo",
                 schema: "jellyfin");
         }
     }

+ 134 - 52
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -1,7 +1,9 @@
 // <auto-generated />
 using System;
+using Jellyfin.Server.Implementations;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
 namespace Jellyfin.Server.Implementations.Migrations
 {
@@ -15,6 +17,31 @@ namespace Jellyfin.Server.Implementations.Migrations
                 .HasDefaultSchema("jellyfin")
                 .HasAnnotation("ProductVersion", "3.1.3");
 
+            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("AccessSchedule");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
                 {
                     b.Property<int>("Id")
@@ -63,12 +90,12 @@ namespace Jellyfin.Server.Implementations.Migrations
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
                 {
-                    b.Property<int>("Id")
+                    b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER");
+                        .HasColumnType("TEXT");
 
-                    b.Property<int?>("Group_Groups_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Group_Groups_Guid")
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("Name")
                         .IsRequired()
@@ -81,9 +108,27 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.HasIndex("Group_Groups_Id");
+                    b.HasIndex("Group_Groups_Guid");
+
+                    b.ToTable("Groups");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
 
-                    b.ToTable("Group");
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ImageInfo");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@@ -95,11 +140,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("Kind")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int?>("Permission_GroupPermissions_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Permission_GroupPermissions_Id")
+                        .HasColumnType("TEXT");
 
-                    b.Property<int?>("Permission_Permissions_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
@@ -112,9 +157,9 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("Permission_GroupPermissions_Id");
 
-                    b.HasIndex("Permission_Permissions_Id");
+                    b.HasIndex("Permission_Permissions_Guid");
 
-                    b.ToTable("Permission");
+                    b.ToTable("Permissions");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@@ -126,8 +171,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("Kind")
                         .HasColumnType("INTEGER");
 
-                    b.Property<int?>("Preference_Preferences_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("Preference_Preferences_Id")
+                        .HasColumnType("TEXT");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
@@ -140,9 +188,11 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
+                    b.HasIndex("Preference_Preferences_Guid");
+
                     b.HasIndex("Preference_Preferences_Id");
 
-                    b.ToTable("Preference");
+                    b.ToTable("Preferences");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b =>
@@ -156,8 +206,8 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasColumnType("TEXT")
                         .HasMaxLength(65535);
 
-                    b.Property<int?>("ProviderMapping_ProviderMappings_Id")
-                        .HasColumnType("INTEGER");
+                    b.Property<Guid?>("ProviderMapping_ProviderMappings_Id")
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("ProviderName")
                         .IsRequired()
@@ -182,12 +232,11 @@ namespace Jellyfin.Server.Implementations.Migrations
 
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
                 {
-                    b.Property<int>("Id")
+                    b.Property<Guid>("Id")
                         .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER");
+                        .HasColumnType("TEXT");
 
                     b.Property<string>("AudioLanguagePreference")
-                        .IsRequired()
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
@@ -196,71 +245,86 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
-                    b.Property<bool?>("DisplayCollectionsView")
+                    b.Property<bool>("DisplayCollectionsView")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("DisplayMissingEpisodes")
+                    b.Property<bool>("DisplayMissingEpisodes")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("EnableNextEpisodeAutoPlay")
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("EnableUserPreferenceAccess")
+                    b.Property<bool>("EnableLocalPassword")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("GroupedFolders")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("HidePlayedInLatest")
+                    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<string>("LatestItemExcludes")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<DateTime>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("LastLoginDate")
+                        .HasColumnType("TEXT");
 
                     b.Property<int?>("LoginAttemptsBeforeLockout")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool>("MustUpdatePassword")
+                    b.Property<int?>("MaxParentalAgeRating")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("MyMediaExcludes")
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
 
-                    b.Property<string>("OrderedViews")
+                    b.Property<string>("Password")
                         .HasColumnType("TEXT")
                         .HasMaxLength(65535);
 
-                    b.Property<string>("Password")
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
                         .HasColumnType("TEXT")
-                        .HasMaxLength(65535);
+                        .HasMaxLength(255);
 
                     b.Property<bool>("PlayDefaultAudioTrack")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("RememberAudioSelections")
+                    b.Property<int?>("ProfileImageId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
                         .HasColumnType("INTEGER");
 
-                    b.Property<bool?>("RememberSubtitleSelections")
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
                         .HasColumnType("INTEGER");
 
                     b.Property<uint>("RowVersion")
                         .IsConcurrencyToken()
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("SubtitleLanguagePrefernce")
+                    b.Property<string>("SubtitleLanguagePreference")
                         .HasColumnType("TEXT")
                         .HasMaxLength(255);
 
-                    b.Property<string>("SubtitleMode")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasMaxLength(255);
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
 
                     b.Property<string>("Username")
                         .IsRequired()
@@ -269,34 +333,45 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("User");
+                    b.HasIndex("ProfileImageId");
+
+                    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.Group", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Groups")
-                        .HasForeignKey("Group_Groups_Id");
+                        .HasForeignKey("Group_Groups_Guid");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.Group", null)
-                        .WithMany("GroupPermissions")
+                        .WithMany("Permissions")
                         .HasForeignKey("Permission_GroupPermissions_Id");
 
                     b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Permissions")
-                        .HasForeignKey("Permission_Permissions_Id");
+                        .HasForeignKey("Permission_Permissions_Guid");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
                 {
-                    b.HasOne("Jellyfin.Data.Entities.Group", null)
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
                         .WithMany("Preferences")
-                        .HasForeignKey("Preference_Preferences_Id");
+                        .HasForeignKey("Preference_Preferences_Guid");
 
-                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                    b.HasOne("Jellyfin.Data.Entities.Group", null)
                         .WithMany("Preferences")
                         .HasForeignKey("Preference_Preferences_Id");
                 });
@@ -311,6 +386,13 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .WithMany("ProviderMappings")
                         .HasForeignKey("ProviderMapping_ProviderMappings_Id");
                 });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage")
+                        .WithMany()
+                        .HasForeignKey("ProfileImageId");
+                });
 #pragma warning restore 612, 618
         }
     }

+ 1 - 1
Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs → Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs

@@ -7,7 +7,7 @@ using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Model.Cryptography;
 
-namespace Jellyfin.Server.Implementations.User
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// The default authentication provider.

+ 3 - 2
Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs → Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Security.Cryptography;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
@@ -10,7 +11,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 
-namespace Jellyfin.Server.Implementations.User
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// The default password reset provider.
@@ -94,7 +95,7 @@ namespace Jellyfin.Server.Implementations.User
         }
 
         /// <inheritdoc />
-        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(Jellyfin.Data.Entities.User user, bool isInNetwork)
+        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
         {
             string pin;
             using (var cryptoRandom = RandomNumberGenerator.Create())

+ 8 - 7
Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs → Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
@@ -9,7 +10,7 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
 
-namespace Jellyfin.Server.Implementations.User
+namespace Jellyfin.Server.Implementations.Users
 {
     public sealed class DeviceAccessEntryPoint : IServerEntryPoint
     {
@@ -33,7 +34,11 @@ namespace Jellyfin.Server.Implementations.User
             return Task.CompletedTask;
         }
 
-        private void OnUserUpdated(object sender, GenericEventArgs<Data.Entities.User> e)
+        public void Dispose()
+        {
+        }
+
+        private void OnUserUpdated(object sender, GenericEventArgs<User> e)
         {
             var user = e.Argument;
             if (!user.HasPermission(PermissionKind.EnableAllDevices))
@@ -42,11 +47,7 @@ namespace Jellyfin.Server.Implementations.User
             }
         }
 
-        public void Dispose()
-        {
-        }
-
-        private void UpdateDeviceAccess(Data.Entities.User user)
+        private void UpdateDeviceAccess(User user)
         {
             var existing = _authRepo.Get(new AuthenticationInfoQuery
             {

+ 1 - 1
Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs → Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs

@@ -1,7 +1,7 @@
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Authentication;
 
-namespace Jellyfin.Server.Implementations.User
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// An invalid authentication provider.

+ 73 - 81
Jellyfin.Server.Implementations/User/UserManager.cs → Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS0067
+#pragma warning disable CA1307
 #pragma warning disable CS1591
 
 using System;
@@ -6,7 +6,9 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Net;
@@ -20,7 +22,7 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Users;
 using Microsoft.Extensions.Logging;
 
-namespace Jellyfin.Server.Implementations.User
+namespace Jellyfin.Server.Implementations.Users
 {
     public class UserManager : IUserManager
     {
@@ -47,24 +49,24 @@ namespace Jellyfin.Server.Implementations.User
             _logger = logger;
         }
 
-        public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserPasswordChanged;
+        public event EventHandler<GenericEventArgs<User>> OnUserPasswordChanged;
 
         /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserUpdated;
+        public event EventHandler<GenericEventArgs<User>> OnUserUpdated;
 
         /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserCreated;
+        public event EventHandler<GenericEventArgs<User>> OnUserCreated;
 
         /// <inheritdoc/>
-        public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserDeleted;
+        public event EventHandler<GenericEventArgs<User>> OnUserDeleted;
 
-        public event EventHandler<GenericEventArgs<Data.Entities.User>> OnUserLockedOut;
+        public event EventHandler<GenericEventArgs<User>> OnUserLockedOut;
 
-        public IEnumerable<Data.Entities.User> Users
+        public IEnumerable<User> Users
         {
             get
             {
-                using var dbContext = _dbProvider.CreateContext();
+                var dbContext = _dbProvider.CreateContext();
                 return dbContext.Users;
             }
         }
@@ -73,37 +75,38 @@ namespace Jellyfin.Server.Implementations.User
         {
             get
             {
-                using var dbContext = _dbProvider.CreateContext();
+                var dbContext = _dbProvider.CreateContext();
                 return dbContext.Users.Select(u => u.Id);
             }
         }
 
-        public Data.Entities.User GetUserById(Guid id)
+        public User GetUserById(Guid id)
         {
             if (id == Guid.Empty)
             {
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
 
-            using var dbContext = _dbProvider.CreateContext();
+            var dbContext = _dbProvider.CreateContext();
 
             return dbContext.Users.Find(id);
         }
 
-        public Data.Entities.User GetUserByName(string name)
+        public User GetUserByName(string name)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
                 throw new ArgumentException("Invalid username", nameof(name));
             }
 
-            using var dbContext = _dbProvider.CreateContext();
+            var dbContext = _dbProvider.CreateContext();
 
-            return dbContext.Users.FirstOrDefault(u =>
-                string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
+            // This can't use an overload with StringComparer because that would cause the query to
+            // have to be evaluated client-side.
+            return dbContext.Users.FirstOrDefault(u => string.Equals(u.Username, name));
         }
 
-        public async Task RenameUser(Data.Entities.User user, string newName)
+        public async Task RenameUser(User user, string newName)
         {
             if (user == null)
             {
@@ -132,43 +135,50 @@ namespace Jellyfin.Server.Implementations.User
             user.Username = newName;
             await UpdateUserAsync(user).ConfigureAwait(false);
 
-            OnUserUpdated?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
+            OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
-        public void UpdateUser(Data.Entities.User user)
+        public void UpdateUser(User user)
         {
-            using var dbContext = _dbProvider.CreateContext();
+            var dbContext = _dbProvider.CreateContext();
             dbContext.Users.Update(user);
             dbContext.SaveChanges();
         }
 
-        public async Task UpdateUserAsync(Data.Entities.User user)
+        public async Task UpdateUserAsync(User user)
         {
-            await using var dbContext = _dbProvider.CreateContext();
+            var dbContext = _dbProvider.CreateContext();
             dbContext.Users.Update(user);
 
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
 
-        public Data.Entities.User CreateUser(string name)
+        public User CreateUser(string name)
         {
-            using var dbContext = _dbProvider.CreateContext();
+            if (!IsValidUsername(name))
+            {
+                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+            }
+
+            var dbContext = _dbProvider.CreateContext();
 
-            var newUser = CreateUserObject(name);
+            var newUser = new User(name, _defaultAuthenticationProvider.GetType().FullName);
             dbContext.Users.Add(newUser);
             dbContext.SaveChanges();
 
+            OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
+
             return newUser;
         }
 
-        public void DeleteUser(Data.Entities.User user)
+        public void DeleteUser(User user)
         {
             if (user == null)
             {
                 throw new ArgumentNullException(nameof(user));
             }
 
-            using var dbContext = _dbProvider.CreateContext();
+            var dbContext = _dbProvider.CreateContext();
 
             if (!dbContext.Users.Contains(user))
             {
@@ -200,19 +210,20 @@ namespace Jellyfin.Server.Implementations.User
 
             dbContext.Users.Remove(user);
             dbContext.SaveChanges();
+            OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
-        public Task ResetPassword(Data.Entities.User user)
+        public Task ResetPassword(User user)
         {
             return ChangePassword(user, string.Empty);
         }
 
-        public void ResetEasyPassword(Data.Entities.User user)
+        public void ResetEasyPassword(User user)
         {
             ChangeEasyPassword(user, string.Empty, null);
         }
 
-        public async Task ChangePassword(Data.Entities.User user, string newPassword)
+        public async Task ChangePassword(User user, string newPassword)
         {
             if (user == null)
             {
@@ -222,24 +233,18 @@ namespace Jellyfin.Server.Implementations.User
             await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
             await UpdateUserAsync(user).ConfigureAwait(false);
 
-            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
+            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
-        public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordSha1)
+        public void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1)
         {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
             GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
-
             UpdateUser(user);
 
-            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
+            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
-        public UserDto GetUserDto(Data.Entities.User user, string remoteEndPoint = null)
+        public UserDto GetUserDto(User user, string remoteEndPoint = null)
         {
             return new UserDto
             {
@@ -271,7 +276,7 @@ namespace Jellyfin.Server.Implementations.User
                     MaxParentalRating = user.MaxParentalAgeRating,
                     EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
                     RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(),
-                    AuthenticatioIsnProviderId = user.AuthenticationProviderId,
+                    AuthenticationProviderId = user.AuthenticationProviderId,
                     PasswordResetProviderId = user.PasswordResetProviderId,
                     InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
                     LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(),
@@ -306,7 +311,7 @@ namespace Jellyfin.Server.Implementations.User
             };
         }
 
-        public PublicUserDto GetPublicUserDto(Data.Entities.User user, string remoteEndPoint = null)
+        public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null)
         {
             if (user == null)
             {
@@ -328,7 +333,7 @@ namespace Jellyfin.Server.Implementations.User
             };
         }
 
-        public async Task<Data.Entities.User> AuthenticateUser(
+        public async Task<User> AuthenticateUser(
             string username,
             string password,
             string passwordSha1,
@@ -341,7 +346,7 @@ namespace Jellyfin.Server.Implementations.User
                 throw new ArgumentNullException(nameof(username));
             }
 
-            var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+            var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
             bool success;
             IAuthenticationProvider authenticationProvider;
 
@@ -370,7 +375,7 @@ namespace Jellyfin.Server.Implementations.User
                     // Search the database for the user again
                     // the authentication provider might have created it
                     user = Users
-                        .FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+                        .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
 
                     if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
                     {
@@ -436,10 +441,10 @@ namespace Jellyfin.Server.Implementations.User
                 if (isUserSession)
                 {
                     user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
-                    UpdateUser(user);
+                    await UpdateUserAsync(user).ConfigureAwait(false);
                 }
 
-                ResetInvalidLoginAttemptCount(user);
+                user.InvalidLoginAttemptCount = 0;
                 _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
             }
             else
@@ -495,14 +500,11 @@ namespace Jellyfin.Server.Implementations.User
         public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
         {
             _authenticationProviders = authenticationProviders.ToArray();
-
-            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
-
-            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
-
             _passwordResetProviders = passwordResetProviders.ToArray();
 
-            _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
+            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
+            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
         }
 
         public NameIdPair[] GetAuthenticationProviders()
@@ -563,7 +565,7 @@ namespace Jellyfin.Server.Implementations.User
             user.MaxParentalAgeRating = policy.MaxParentalRating;
             user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
             user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
-            user.AuthenticationProviderId = policy.AuthenticatioIsnProviderId;
+            user.AuthenticationProviderId = policy.AuthenticationProviderId;
             user.PasswordResetProviderId = policy.PasswordResetProviderId;
             user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
             user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1
@@ -604,28 +606,25 @@ namespace Jellyfin.Server.Implementations.User
             user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
         }
 
-        private Data.Entities.User CreateUserObject(string name)
+        private bool IsValidUsername(string name)
         {
-            return new Data.Entities.User(
-                username: name,
-                mustUpdatePassword: false,
-                authenticationProviderId: _defaultAuthenticationProvider.GetType().FullName,
-                invalidLoginAttemptCount: -1,
-                subtitleMode: SubtitlePlaybackMode.Default,
-                playDefaultAudioTrack: true);
+            // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
+            // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
+            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
+            return Regex.IsMatch(name, @"^[\w\-'._@]*$");
         }
 
-        private IAuthenticationProvider GetAuthenticationProvider(Data.Entities.User user)
+        private IAuthenticationProvider GetAuthenticationProvider(User user)
         {
             return GetAuthenticationProviders(user)[0];
         }
 
-        private IPasswordResetProvider GetPasswordResetProvider(Data.Entities.User user)
+        private IPasswordResetProvider GetPasswordResetProvider(User user)
         {
             return GetPasswordResetProviders(user)[0];
         }
 
-        private IList<IAuthenticationProvider> GetAuthenticationProviders(Data.Entities.User user)
+        private IList<IAuthenticationProvider> GetAuthenticationProviders(User user)
         {
             var authenticationProviderId = user?.AuthenticationProviderId;
 
@@ -640,7 +639,7 @@ namespace Jellyfin.Server.Implementations.User
             {
                 // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
                 _logger.LogWarning(
-                    "User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
+                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
                     user?.Username,
                     user?.AuthenticationProviderId);
                 providers = new List<IAuthenticationProvider>
@@ -652,7 +651,7 @@ namespace Jellyfin.Server.Implementations.User
             return providers;
         }
 
-        private IList<IPasswordResetProvider> GetPasswordResetProviders(Data.Entities.User user)
+        private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
         {
             var passwordResetProviderId = user?.PasswordResetProviderId;
             var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
@@ -675,11 +674,10 @@ namespace Jellyfin.Server.Implementations.User
             return providers;
         }
 
-        private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)>
-            AuthenticateLocalUser(
+        private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(
                 string username,
                 string password,
-                Jellyfin.Data.Entities.User user,
+                User user,
                 string remoteEndPoint)
         {
             bool success = false;
@@ -721,7 +719,7 @@ namespace Jellyfin.Server.Implementations.User
             IAuthenticationProvider provider,
             string username,
             string password,
-            Data.Entities.User resolvedUser)
+            User resolvedUser)
         {
             try
             {
@@ -745,27 +743,21 @@ namespace Jellyfin.Server.Implementations.User
             }
         }
 
-        private void IncrementInvalidLoginAttemptCount(Data.Entities.User user)
+        private void IncrementInvalidLoginAttemptCount(User user)
         {
             int invalidLogins = user.InvalidLoginAttemptCount;
             int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
-            if (maxInvalidLogins.HasValue
-                && invalidLogins >= maxInvalidLogins)
+            if (maxInvalidLogins.HasValue && invalidLogins >= maxInvalidLogins)
             {
                 user.SetPermission(PermissionKind.IsDisabled, true);
-                OnUserLockedOut?.Invoke(this, new GenericEventArgs<Data.Entities.User>(user));
+                OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
                 _logger.LogWarning(
-                    "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
+                    "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
                     user.Username,
                     invalidLogins);
             }
 
             UpdateUser(user);
         }
-
-        private void ResetInvalidLoginAttemptCount(Data.Entities.User user)
-        {
-            user.InvalidLoginAttemptCount = 0;
-        }
     }
 }

+ 6 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -7,8 +7,10 @@ using Emby.Server.Implementations;
 using Jellyfin.Drawing.Skia;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
+using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
 using Microsoft.EntityFrameworkCore;
@@ -69,6 +71,7 @@ namespace Jellyfin.Server
             serviceCollection.AddSingleton<JellyfinDbProvider>();
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+            serviceCollection.AddSingleton<IUserManager, UserManager>();
 
             base.RegisterServices(serviceCollection);
         }
@@ -80,6 +83,9 @@ namespace Jellyfin.Server
         protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
         {
             yield return typeof(CoreAppHost).Assembly;
+            yield return typeof(DefaultAuthenticationProvider).Assembly;
+            yield return typeof(DefaultPasswordResetProvider).Assembly;
+            yield return typeof(InvalidAuthProvider).Assembly;
         }
 
         /// <inheritdoc />

+ 0 - 1
Jellyfin.Server/Jellyfin.Server.csproj

@@ -41,7 +41,6 @@
 
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.7.82" />
-    <PackageReference Include="Json.Net" Version="1.0.22" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
     <PackageReference Include="prometheus-net" Version="3.5.0" />

+ 73 - 27
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -1,33 +1,45 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
+using System;
 using System.IO;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Serialization;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Controller;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Users;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
 using SQLitePCL.pretty;
+using JsonSerializer = System.Text.Json.JsonSerializer;
 
 namespace Jellyfin.Server.Migrations.Routines
 {
+    /// <summary>
+    /// The migration routine for migrating the user database to EF Core.
+    /// </summary>
     public class MigrateUserDb : IMigrationRoutine
     {
-        private readonly ILogger<MigrateUserDb> _logger;
+        private const string DbFilename = "users.db";
 
+        private readonly ILogger<MigrateUserDb> _logger;
         private readonly IServerApplicationPaths _paths;
-
         private readonly JellyfinDbProvider _provider;
-
         private readonly MyXmlSerializer _xmlSerializer;
 
-        public MigrateUserDb(ILogger<MigrateUserDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider, MyXmlSerializer xmlSerializer)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="paths">The server application paths.</param>
+        /// <param name="provider">The database provider.</param>
+        /// <param name="xmlSerializer">The xml serializer.</param>
+        public MigrateUserDb(
+            ILogger<MigrateUserDb> logger,
+            IServerApplicationPaths paths,
+            JellyfinDbProvider provider,
+            MyXmlSerializer xmlSerializer)
         {
             _logger = logger;
             _paths = paths;
@@ -35,18 +47,21 @@ namespace Jellyfin.Server.Migrations.Routines
             _xmlSerializer = xmlSerializer;
         }
 
+        /// <inheritdoc/>
         public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
 
-        public string Name => "MigrateUserDb";
+        /// <inheritdoc/>
+        public string Name => "MigrateUserDatabase";
 
+        /// <inheritdoc/>
         public void Perform()
         {
             var dataPath = _paths.DataPath;
             _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
 
-            using (var connection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null))
+            using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
             {
-                using var dbContext = _provider.CreateContext();
+                var dbContext = _provider.CreateContext();
 
                 var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
 
@@ -55,26 +70,30 @@ namespace Jellyfin.Server.Migrations.Routines
 
                 foreach (var entry in queryResult)
                 {
-                    var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(entry[2].ToString());
-                    var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, json["Name"]);
-
-                    var config = (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"));
-                    var policy = (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"));
-
-                    var user = new User(
-                        json["Name"],
-                        false,
-                        policy.AuthenticatioIsnProviderId,
-                        policy.InvalidLoginAttemptCount,
-                        config.SubtitleMode,
-                        config.PlayDefaultAudioTrack)
+                    UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob());
+                    var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
+
+                    var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
+                        ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
+                        : new UserConfiguration();
+                    var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
+                        ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
+                        : new UserPolicy();
+                    policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
+                        "Emby.Server.Implementations.Library",
+                        "Jellyfin.Server.Implementations.Users",
+                        StringComparison.Ordinal)
+                        ?? typeof(DefaultAuthenticationProvider).FullName;
+
+                    policy.PasswordResetProviderId ??= typeof(DefaultPasswordResetProvider).FullName;
+
+                    var user = new User(mockup.Name, policy.AuthenticationProviderId)
                     {
                         Id = entry[1].ReadGuidFromBlob(),
                         InternalId = entry[0].ToInt64(),
                         MaxParentalAgeRating = policy.MaxParentalRating,
                         EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
                         RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
-                        AuthenticationProviderId = policy.AuthenticatioIsnProviderId,
                         PasswordResetProviderId = policy.PasswordResetProviderId,
                         InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
                         LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 ? null : new int?(policy.LoginAttemptsBeforeLockout),
@@ -89,6 +108,10 @@ namespace Jellyfin.Server.Migrations.Routines
                         EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
                         RememberSubtitleSelections = config.RememberSubtitleSelections,
                         SubtitleLanguagePreference = config.SubtitleLanguagePreference,
+                        Password = mockup.Password,
+                        EasyPassword = mockup.EasyPassword,
+                        LastLoginDate = mockup.LastLoginDate ?? DateTime.MinValue,
+                        LastActivityDate = mockup.LastActivityDate ?? DateTime.MinValue
                     };
 
                     user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
@@ -112,6 +135,7 @@ namespace Jellyfin.Server.Migrations.Routines
                     user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
                     user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
                     user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
                     foreach (var policyAccessSchedule in policy.AccessSchedules)
                     {
                         user.AccessSchedules.Add(policyAccessSchedule);
@@ -126,6 +150,8 @@ namespace Jellyfin.Server.Migrations.Routines
                     user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
                     user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
                     user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+                    dbContext.Users.Add(user);
                 }
 
                 dbContext.SaveChanges();
@@ -133,12 +159,32 @@ namespace Jellyfin.Server.Migrations.Routines
 
             try
             {
-                File.Move(Path.Combine(dataPath, "users.db"), Path.Combine(dataPath, "users.db" + ".old"));
+                File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+                var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+                if (File.Exists(journalPath))
+                {
+                    File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
+                }
             }
             catch (IOException e)
             {
                 _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
             }
         }
+
+#nullable disable
+        internal class UserMockup
+        {
+            public string Password { get; set; }
+
+            public string EasyPassword { get; set; }
+
+            public DateTime? LastLoginDate { get; set; }
+
+            public DateTime? LastActivityDate { get; set; }
+
+            public string Name { get; set; }
+        }
     }
 }

+ 6 - 2
MediaBrowser.Model/Users/UserPolicy.cs

@@ -1,6 +1,8 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Text.Json.Serialization;
+using System.Xml.Serialization;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 
@@ -34,7 +36,7 @@ namespace MediaBrowser.Model.Users
 
         public string[] BlockedTags { get; set; }
         public bool EnableUserPreferenceAccess { get; set; }
-        public Jellyfin.Data.Entities.AccessSchedule[] AccessSchedules { get; set; }
+        public AccessSchedule[] AccessSchedules { get; set; }
         public UnratedItem[] BlockUnratedItems { get; set; }
         public bool EnableRemoteControlOfOtherUsers { get; set; }
         public bool EnableSharedDeviceControl { get; set; }
@@ -78,7 +80,9 @@ namespace MediaBrowser.Model.Users
         public string[] BlockedChannels { get; set; }
 
         public int RemoteClientBitrateLimit { get; set; }
-        public string AuthenticatioIsnProviderId { get; set; }
+
+        [XmlElement(ElementName = "AuthenticationProviderId")]
+        public string AuthenticationProviderId { get; set; }
         public string PasswordResetProviderId { get; set; }
 
         public UserPolicy()