浏览代码

Merge remote-tracking branch 'upstream/master' into HEAD

crobibero 5 年之前
父节点
当前提交
589735f60c
共有 100 个文件被更改,包括 3476 次插入2250 次删除
  1. 2 0
      CONTRIBUTORS.md
  2. 5 3
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  3. 20 13
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  4. 9 5
      Emby.Dlna/Didl/DidlBuilder.cs
  5. 9 1
      Emby.Dlna/PlayTo/PlayToController.cs
  6. 9 0
      Emby.Drawing/ImageProcessor.cs
  7. 5 3
      Emby.Notifications/NotificationManager.cs
  8. 22 34
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  9. 1 9
      Emby.Server.Implementations/ApplicationHost.cs
  10. 8 3
      Emby.Server.Implementations/Channels/ChannelManager.cs
  11. 1 0
      Emby.Server.Implementations/Collections/CollectionManager.cs
  12. 1 1
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  13. 1 0
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  14. 0 240
      Emby.Server.Implementations/Data/SqliteUserRepository.cs
  15. 9 20
      Emby.Server.Implementations/Devices/DeviceManager.cs
  16. 12 4
      Emby.Server.Implementations/Dto/DtoService.cs
  17. 1 0
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  18. 2 1
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  19. 0 77
      Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
  20. 5 23
      Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  21. 0 1
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  22. 12 10
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  23. 2 2
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  24. 1 1
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  25. 6 4
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  26. 7 2
      Emby.Server.Implementations/Library/LibraryManager.cs
  27. 23 24
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  28. 1 1
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  29. 7 11
      Emby.Server.Implementations/Library/MusicManager.cs
  30. 3 0
      Emby.Server.Implementations/Library/SearchEngine.cs
  31. 2 0
      Emby.Server.Implementations/Library/UserDataManager.cs
  32. 0 1107
      Emby.Server.Implementations/Library/UserManager.cs
  33. 20 10
      Emby.Server.Implementations/Library/UserViewManager.cs
  34. 12 13
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  35. 2 1
      Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
  36. 3 0
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  37. 52 30
      Emby.Server.Implementations/Session/SessionManager.cs
  38. 1 0
      Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
  39. 1 0
      Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
  40. 1 0
      Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
  41. 1 0
      Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
  42. 1 0
      Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
  43. 1 0
      Emby.Server.Implementations/Sorting/PlayCountComparer.cs
  44. 27 52
      Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
  45. 9 5
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  46. 3 2
      Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
  47. 9 3
      Jellyfin.Api/Controllers/StartupController.cs
  48. 65 0
      Jellyfin.Data/DayOfWeekHelper.cs
  49. 91 0
      Jellyfin.Data/Entities/AccessSchedule.cs
  50. 58 45
      Jellyfin.Data/Entities/Group.cs
  51. 30 0
      Jellyfin.Data/Entities/ImageInfo.cs
  52. 45 92
      Jellyfin.Data/Entities/Permission.cs
  53. 44 56
      Jellyfin.Data/Entities/Preference.cs
  54. 0 6
      Jellyfin.Data/Entities/ProviderMapping.cs
  55. 6 14
      Jellyfin.Data/Entities/Series.cs
  56. 386 114
      Jellyfin.Data/Entities/User.cs
  57. 1 1
      Jellyfin.Data/Enums/DynamicDayOfWeek.cs
  58. 107 20
      Jellyfin.Data/Enums/PermissionKind.cs
  59. 62 7
      Jellyfin.Data/Enums/PreferenceKind.cs
  60. 2 2
      Jellyfin.Data/Enums/SubtitlePlaybackMode.cs
  61. 4 4
      Jellyfin.Data/Enums/SyncPlayAccess.cs
  62. 1 1
      Jellyfin.Data/Enums/UnratedItem.cs
  63. 0 13
      Jellyfin.Data/Enums/Weekday.cs
  64. 31 0
      Jellyfin.Data/IHasPermissions.cs
  65. 1 0
      Jellyfin.Data/Jellyfin.Data.csproj
  66. 0 2
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  67. 39 31
      Jellyfin.Server.Implementations/JellyfinDb.cs
  68. 1 1
      Jellyfin.Server.Implementations/JellyfinDbProvider.cs
  69. 312 0
      Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
  70. 197 0
      Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs
  71. 243 1
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  72. 9 7
      Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
  73. 30 29
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  74. 67 0
      Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
  75. 4 8
      Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
  76. 841 0
      Jellyfin.Server.Implementations/Users/UserManager.cs
  77. 11 2
      Jellyfin.Server/CoreAppHost.cs
  78. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  79. 208 0
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  80. 3 2
      MediaBrowser.Api/BaseApiService.cs
  81. 1 0
      MediaBrowser.Api/FilterService.cs
  82. 144 16
      MediaBrowser.Api/Images/ImageService.cs
  83. 11 5
      MediaBrowser.Api/Library/LibraryService.cs
  84. 2 1
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  85. 9 2
      MediaBrowser.Api/Movies/MoviesService.cs
  86. 1 0
      MediaBrowser.Api/Music/InstantMixService.cs
  87. 2 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  88. 22 14
      MediaBrowser.Api/Playback/MediaInfoService.cs
  89. 3 2
      MediaBrowser.Api/Sessions/SessionService.cs
  90. 1 0
      MediaBrowser.Api/SuggestionsService.cs
  91. 4 4
      MediaBrowser.Api/TvShowsService.cs
  92. 1 0
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  93. 12 7
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  94. 1 1
      MediaBrowser.Api/UserLibrary/PlaystateService.cs
  95. 1 1
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  96. 21 16
      MediaBrowser.Api/UserService.cs
  97. 1 1
      MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
  98. 1 1
      MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
  99. 7 3
      MediaBrowser.Controller/Channels/Channel.cs
  100. 1 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs

+ 2 - 0
CONTRIBUTORS.md

@@ -7,6 +7,7 @@
  - [anthonylavado](https://github.com/anthonylavado)
  - [Artiume](https://github.com/Artiume)
  - [AThomsen](https://github.com/AThomsen)
+ - [barronpm](https://github.com/barronpm)
  - [bilde2910](https://github.com/bilde2910)
  - [bfayers](https://github.com/bfayers)
  - [BnMcG](https://github.com/BnMcG)
@@ -130,6 +131,7 @@
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
+ - [Pusta](https://github.com/pusta)
 
 # Emby Contributors
 

+ 5 - 3
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -4,11 +4,12 @@ using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.TV;
@@ -32,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ITVSeriesManager _tvSeriesManager;
 
-        public ContentDirectory(IDlnaManager dlna,
+        public ContentDirectory(
+            IDlnaManager dlna,
             IUserDataManager userDataManager,
             IImageProcessor imageProcessor,
             ILibraryManager libraryManager,
@@ -131,7 +133,7 @@ namespace Emby.Dlna.ContentDirectory
 
             foreach (var user in _userManager.Users)
             {
-                if (user.Policy.IsAdministrator)
+                if (user.HasPermission(PermissionKind.IsAdministrator))
                 {
                     return user;
                 }

+ 20 - 13
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -10,6 +10,7 @@ using System.Threading;
 using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
@@ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
@@ -28,6 +28,12 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Dlna.ContentDirectory
 {
@@ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory
                 return GetGenres(item, user, query);
             }
 
-            var array = new ServerItem[]
+            var array = new[]
             {
                 new ServerItem(item)
                 {
@@ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
         {
             query.Parent = null;
-            query.IncludeItemTypes = new[] { typeof(Playlist).Name };
+            query.IncludeItemTypes = new[] { nameof(Playlist) };
             query.SetUser(user);
             query.Recursive = true;
 
@@ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory
             {
                 UserId = user.Id,
                 Limit = 50,
-                IncludeItemTypes = new[] { typeof(Audio).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
+                IncludeItemTypes = new[] { nameof(Audio) },
+                ParentId = parent?.Id ?? Guid.Empty,
                 GroupItems = true
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
@@ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory
                 Limit = query.Limit,
                 StartIndex = query.StartIndex,
                 UserId = query.User.Id
-
             }, new[] { parent }, query.DtoOptions);
 
             return ToResult(result);
@@ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory
                 IncludeItemTypes = new[] { typeof(Episode).Name },
                 ParentId = parent == null ? Guid.Empty : parent.Id,
                 GroupItems = false
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
@@ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
-            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
+            var items = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
             {
                 UserId = user.Id,
                 Limit = 50,
-                IncludeItemTypes = new[] { typeof(Movie).Name },
-                ParentId = parent == null ? Guid.Empty : parent.Id,
+                IncludeItemTypes = new[] { nameof(Movie) },
+                ParentId = parent?.Id ?? Guid.Empty,
                 GroupItems = true
-
             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
 
             return ToResult(items);
@@ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name },
+                IncludeItemTypes = new[]
+                {
+                    nameof(Movie),
+                    nameof(Series)
+                },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()

+ 9 - 5
Emby.Dlna/Didl/DidlBuilder.cs

@@ -6,14 +6,13 @@ using System.IO;
 using System.Linq;
 using System.Text;
 using System.Xml;
-using Emby.Dlna.Configuration;
 using Emby.Dlna.ContentDirectory;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Playlists;
@@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
+using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
 
 namespace Emby.Dlna.Didl
 {
@@ -421,7 +427,6 @@ namespace Emby.Dlna.Didl
                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
                     case StubType.Series: return _localization.GetLocalizedString("Shows");
-                    default: break;
                 }
             }
 
@@ -670,7 +675,7 @@ namespace Emby.Dlna.Didl
                 return;
             }
 
-            MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
+            XmlAttribute secAttribute = null;
             foreach (var attribute in _profile.XmlRootAttributes)
             {
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -995,7 +1000,6 @@ namespace Emby.Dlna.Didl
             }
 
             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
-
         }
 
         private void AddImageResElement(

+ 9 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.Didl;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 namespace Emby.Dlna.PlayTo
 {
@@ -446,7 +448,13 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+        private PlaylistItem CreatePlaylistItem(
+            BaseItem item,
+            User user,
+            long startPostionTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex)
         {
             var deviceInfo = _device.Properties;
 

+ 9 - 0
Emby.Drawing/ImageProcessor.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
@@ -14,6 +15,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
 
 namespace Emby.Drawing
 {
@@ -349,6 +351,13 @@ namespace Emby.Drawing
             });
         }
 
+        /// <inheritdoc />
+        public string GetImageCacheTag(User user)
+        {
+            return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
+                .ToString("N", CultureInfo.InvariantCulture);
+        }
+
         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
         {
             var inputFormat = Path.GetExtension(originalImagePath)

+ 5 - 3
Emby.Notifications/NotificationManager.cs

@@ -4,6 +4,8 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -101,7 +103,7 @@ namespace Emby.Notifications
                 switch (request.SendToUserMode.Value)
                 {
                     case SendToUserType.Admins:
-                        return _userManager.Users.Where(i => i.Policy.IsAdministrator)
+                        return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
                                 .Select(i => i.Id);
                     case SendToUserType.All:
                         return _userManager.UsersIds;
@@ -117,7 +119,7 @@ namespace Emby.Notifications
                 var config = GetConfiguration();
 
                 return _userManager.Users
-                    .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy))
+                    .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
                     .Select(i => i.Id);
             }
 
@@ -142,7 +144,7 @@ namespace Emby.Notifications
                 User = user
             };
 
-            _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name);
+            _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
 
             try
             {

+ 22 - 34
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity
 
             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
 
-            _userManager.UserCreated += OnUserCreated;
-            _userManager.UserPasswordChanged += OnUserPasswordChanged;
-            _userManager.UserDeleted += OnUserDeleted;
-            _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
-            _userManager.UserLockedOut += OnUserLockedOut;
+            _userManager.OnUserCreated += OnUserCreated;
+            _userManager.OnUserPasswordChanged += OnUserPasswordChanged;
+            _userManager.OnUserDeleted += OnUserDeleted;
+            _userManager.OnUserLockedOut += OnUserLockedOut;
 
             return Task.CompletedTask;
         }
 
-        private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                     string.Format(
                         CultureInfo.InvariantCulture,
                         _localization.GetLocalizedString("UserLockedOutWithName"),
-                        e.Argument.Name),
+                        e.Argument.Username),
                     NotificationType.UserLockedOut.ToString(),
-                    e.Argument.Id))
-                .ConfigureAwait(false);
+                    e.Argument.Id)
+            {
+                LogSeverity = LogLevel.Error
+            }).ConfigureAwait(false);
         }
 
         private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
@@ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
-                    user.Name,
+                    user.Username,
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackStoppedNotificationType(item.MediaType),
@@ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
-                    user.Name,
+                    user.Username,
                     GetItemName(item),
                     e.DeviceName),
                 GetPlaybackNotificationType(item.MediaType),
@@ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity
             }).ConfigureAwait(false);
         }
 
-        private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
-        {
-            await CreateLogEntry(new ActivityLog(
-                string.Format(
-                    CultureInfo.InvariantCulture,
-                    _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
-                    e.Argument.Name),
-                "UserPolicyUpdated",
-                e.Argument.Id))
-                .ConfigureAwait(false);
-        }
-
-        private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserDeletedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserDeleted",
                 Guid.Empty))
                 .ConfigureAwait(false);
         }
 
-        private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserPasswordChangedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserPasswordChanged",
                 e.Argument.Id))
                 .ConfigureAwait(false);
         }
 
-        private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
+        private async void OnUserCreated(object sender, GenericEventArgs<User> e)
         {
             await CreateLogEntry(new ActivityLog(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("UserCreatedWithName"),
-                    e.Argument.Name),
+                    e.Argument.Username),
                 "UserCreated",
                 e.Argument.Id))
                 .ConfigureAwait(false);
@@ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity
 
             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
 
-            _userManager.UserCreated -= OnUserCreated;
-            _userManager.UserPasswordChanged -= OnUserPasswordChanged;
-            _userManager.UserDeleted -= OnUserDeleted;
-            _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
-            _userManager.UserLockedOut -= OnUserLockedOut;
+            _userManager.OnUserCreated -= OnUserCreated;
+            _userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
+            _userManager.OnUserDeleted -= OnUserDeleted;
+            _userManager.OnUserLockedOut -= OnUserLockedOut;
         }
 
         /// <summary>

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

@@ -562,11 +562,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
-            serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
-
             // 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
@@ -659,15 +656,11 @@ namespace Emby.Server.Implementations
 
             ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
-            ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
 
             SetStaticProperties();
 
-            var userManager = (UserManager)Resolve<IUserManager>();
-            userManager.Initialize();
-
             var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
+            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
 
             FindParts();
         }
@@ -750,7 +743,6 @@ namespace Emby.Server.Implementations
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
-            User.UserManager = Resolve<IUserManager>();
             BaseItem.FileSystem = _fileSystemManager;
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();

+ 8 - 3
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -6,6 +6,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
@@ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Channels;
@@ -24,6 +23,11 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Server.Implementations.Channels
 {
@@ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels
             return result;
         }
 
-        private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
+        private async Task<ChannelItemResult> GetChannelItems(
+            IChannel channel,
             User user,
             string externalFolderId,
             ChannelItemSortField? sortField,

+ 1 - 0
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;

+ 1 - 1
Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs

@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;

+ 0 - 240
Emby.Server.Implementations/Data/SqliteUserRepository.cs

@@ -1,240 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.Json;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    /// <summary>
-    /// Class SQLiteUserRepository
-    /// </summary>
-    public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
-    {
-        private readonly JsonSerializerOptions _jsonOptions;
-
-        public SqliteUserRepository(
-            ILogger<SqliteUserRepository> logger,
-            IServerApplicationPaths appPaths)
-            : base(logger)
-        {
-            _jsonOptions = JsonDefaults.GetOptions();
-
-            DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
-        }
-
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => "SQLite";
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        public void Initialize()
-        {
-            using (var connection = GetConnection())
-            {
-                var localUsersTableExists = TableExists(connection, "LocalUsersv2");
-
-                connection.RunQueries(new[] {
-                    "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
-                    "drop index if exists idx_users"
-                });
-
-                if (!localUsersTableExists && TableExists(connection, "Users"))
-                {
-                    TryMigrateToLocalUsersTable(connection);
-                }
-
-                RemoveEmptyPasswordHashes(connection);
-            }
-        }
-
-        private void TryMigrateToLocalUsersTable(ManagedConnection connection)
-        {
-            try
-            {
-                connection.RunQueries(new[]
-                {
-                    "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
-                });
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error migrating users database");
-            }
-        }
-
-        private void RemoveEmptyPasswordHashes(ManagedConnection connection)
-        {
-            foreach (var user in RetrieveAllUsers(connection))
-            {
-                // If the user password is the sha1 hash of the empty string, remove it
-                if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
-                    && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
-                {
-                    continue;
-                }
-
-                user.Password = null;
-                var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
-                    {
-                        statement.TryBind("@InternalId", user.InternalId);
-                        statement.TryBind("@data", serialized);
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-
-        /// <summary>
-        /// Save a user in the repo
-        /// </summary>
-        public void CreateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
-                    {
-                        statement.TryBind("@guid", user.Id.ToByteArray());
-                        statement.TryBind("@data", serialized);
-
-                        statement.MoveNext();
-                    }
-
-                    var createdUser = GetUser(user.Id, connection);
-
-                    if (createdUser == null)
-                    {
-                        throw new ApplicationException("created user should never be null");
-                    }
-
-                    user.InternalId = createdUser.InternalId;
-
-                }, TransactionMode);
-            }
-        }
-
-        public void UpdateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
-                    {
-                        statement.TryBind("@InternalId", user.InternalId);
-                        statement.TryBind("@data", serialized);
-                        statement.MoveNext();
-                    }
-
-                }, TransactionMode);
-            }
-        }
-
-        private User GetUser(Guid guid, ManagedConnection connection)
-        {
-            using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
-            {
-                statement.TryBind("@guid", guid);
-
-                foreach (var row in statement.ExecuteQuery())
-                {
-                    return GetUser(row);
-                }
-            }
-
-            return null;
-        }
-
-        private User GetUser(IReadOnlyList<IResultSetValue> row)
-        {
-            var id = row[0].ToInt64();
-            var guid = row[1].ReadGuidFromBlob();
-
-            var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions);
-            user.InternalId = id;
-            user.Id = guid;
-            return user;
-        }
-
-        /// <summary>
-        /// Retrieve all users from the database
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        public List<User> RetrieveAllUsers()
-        {
-            using (var connection = GetConnection(true))
-            {
-                return new List<User>(RetrieveAllUsers(connection));
-            }
-        }
-
-        /// <summary>
-        /// Retrieve all users from the database
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
-        {
-            foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
-            {
-                yield return GetUser(row);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
-        public void DeleteUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(db =>
-                {
-                    using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
-                    {
-                        statement.TryBind("@id", user.InternalId);
-                        statement.MoveNext();
-                    }
-                }, TransactionMode);
-            }
-        }
-    }
-}

+ 9 - 20
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -5,10 +5,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Devices;
@@ -16,7 +17,6 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
-using MediaBrowser.Model.Users;
 
 namespace Emby.Server.Implementations.Devices
 {
@@ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices
         private readonly IServerConfigurationManager _config;
         private readonly IAuthenticationRepository _authRepo;
         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
+        private readonly object _capabilitiesSyncLock = new object();
 
         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
 
-        private readonly object _capabilitiesSyncLock = new object();
-
         public DeviceManager(
             IAuthenticationRepository authRepo,
             IJsonSerializer json,
@@ -175,7 +174,12 @@ namespace Emby.Server.Implementations.Devices
                 throw new ArgumentNullException(nameof(deviceId));
             }
 
-            if (!CanAccessDevice(user.Policy, deviceId))
+            if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+            {
+                return true;
+            }
+
+            if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
             {
                 var capabilities = GetCapabilities(deviceId);
 
@@ -187,20 +191,5 @@ namespace Emby.Server.Implementations.Devices
 
             return true;
         }
-
-        private static bool CanAccessDevice(UserPolicy policy, string id)
-        {
-            if (policy.EnableAllDevices)
-            {
-                return true;
-            }
-
-            if (policy.IsAdministrator)
-            {
-                return true;
-            }
-
-            return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
-        }
     }
 }

+ 12 - 4
Emby.Server.Implementations/Dto/DtoService.cs

@@ -6,14 +6,14 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
@@ -24,6 +24,14 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Person = MediaBrowser.Controller.Entities.Person;
+using Photo = MediaBrowser.Controller.Entities.Photo;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Server.Implementations.Dto
 {
@@ -384,7 +392,7 @@ namespace Emby.Server.Implementations.Dto
 
                     if (options.ContainsField(ItemFields.ChildCount))
                     {
-                        dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user);
+                        dto.ChildCount ??= GetChildCount(folder, user);
                     }
                 }
 
@@ -414,7 +422,7 @@ namespace Emby.Server.Implementations.Dto
 
             if (options.ContainsField(ItemFields.BasicSyncInfo))
             {
-                var userCanSync = user != null && user.Policy.EnableContentDownloading;
+                var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
                 if (userCanSync && item.SupportsExternalTransfer)
                 {
                     dto.SupportsSync = true;

+ 1 - 0
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;

+ 2 - 1
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -4,6 +4,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private async Task SendMessage(string name, TimerEventInfo info)
         {
-            var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
+            var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
 
             try
             {

+ 0 - 77
Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs

@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Class RefreshUsersMetadata.
-    /// </summary>
-    public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
-        /// </summary>
-        public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
-        {
-            _userManager = userManager;
-            _fileSystem = fileSystem;
-        }
-
-        /// <inheritdoc />
-        public string Name => "Refresh Users";
-
-        /// <inheritdoc />
-        public string Key => "RefreshUsers";
-
-        /// <inheritdoc />
-        public string Description => "Refresh user infos";
-
-        /// <inheritdoc />
-        public string Category => "Library";
-
-        /// <inheritdoc />
-        public bool IsHidden => true;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
-
-        /// <inheritdoc />
-        public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            foreach (var user in _userManager.Users)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        /// <inheritdoc />
-        public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
-        {
-            return new[]
-            {
-                new TaskTriggerInfo
-                {
-                    IntervalTicks = TimeSpan.FromDays(1).Ticks,
-                    Type = TaskTriggerInfo.TriggerInterval
-                }
-            };
-        }
-    }
-}

+ 5 - 23
Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -3,10 +3,10 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
@@ -68,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         public Task RunAsync()
         {
-            _userManager.UserDeleted += OnUserDeleted;
-            _userManager.UserUpdated += OnUserUpdated;
-            _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
-            _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated;
+            _userManager.OnUserDeleted += OnUserDeleted;
+            _userManager.OnUserUpdated += OnUserUpdated;
 
             _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
 
@@ -153,20 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints
             await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
         }
 
-        private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false);
-        }
-
-        private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var dto = _userManager.GetUserDto(e.Argument);
-
-            await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false);
-        }
-
         private async Task SendMessageToAdminSessions<T>(string name, T data)
         {
             try
@@ -210,10 +194,8 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             if (dispose)
             {
-                _userManager.UserDeleted -= OnUserDeleted;
-                _userManager.UserUpdated -= OnUserUpdated;
-                _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
-                _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated;
+                _userManager.OnUserDeleted -= OnUserDeleted;
+                _userManager.OnUserUpdated -= OnUserUpdated;
 
                 _installationManager.PluginUninstalled -= OnPluginUninstalled;
                 _installationManager.PackageInstalling -= OnPackageInstalling;

+ 0 - 1
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Text;
 using MediaBrowser.Controller.Net;

+ 12 - 10
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -3,10 +3,11 @@
 using System;
 using System.Linq;
 using Emby.Server.Implementations.SocketSharp;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
@@ -90,7 +91,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 !string.IsNullOrEmpty(auth.Client) &&
                 !string.IsNullOrEmpty(auth.Device))
             {
-                _sessionManager.LogSessionActivity(auth.Client,
+                _sessionManager.LogSessionActivity(
+                    auth.Client,
                     auth.Version,
                     auth.DeviceId,
                     auth.Device,
@@ -104,21 +106,21 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private void ValidateUserAccess(
             User user,
             IRequest request,
-            IAuthenticationAttributes authAttribtues,
+            IAuthenticationAttributes authAttributes,
             AuthorizationInfo auth)
         {
-            if (user.Policy.IsDisabled)
+            if (user.HasPermission(PermissionKind.IsDisabled))
             {
                 throw new SecurityException("User account has been disabled.");
             }
 
-            if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
+            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
             {
                 throw new SecurityException("User account has been disabled.");
             }
 
-            if (!user.Policy.IsAdministrator
-                && !authAttribtues.EscapeParentalControl
+            if (!user.HasPermission(PermissionKind.IsAdministrator)
+                && !authAttributes.EscapeParentalControl
                 && !user.IsParentalScheduleAllowed())
             {
                 request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
@@ -186,7 +188,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         {
             if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.IsAdministrator)
+                if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
                 {
                     throw new SecurityException("User does not have admin access.");
                 }
@@ -194,7 +196,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.EnableContentDeletion)
+                if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
                 {
                     throw new SecurityException("User does not have delete access.");
                 }
@@ -202,7 +204,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
             {
-                if (user == null || !user.Policy.EnableContentDownloading)
+                if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
                 {
                     throw new SecurityException("User does not have download access.");
                 }

+ 2 - 2
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -149,9 +149,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
                     {
                         info.User = _userManager.GetUserById(tokenInfo.UserId);
 
-                        if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+                        if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
                         {
-                            tokenInfo.UserName = info.User.Name;
+                            tokenInfo.UserName = info.User.Username;
                             updateToken = true;
                         }
                     }

+ 1 - 1
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;

+ 6 - 4
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -234,10 +234,12 @@ namespace Emby.Server.Implementations.HttpServer
         private Task SendKeepAliveResponse()
         {
             LastKeepAliveDate = DateTime.UtcNow;
-            return SendAsync(new WebSocketMessage<string>
-            {
-                MessageType = "KeepAlive"
-            }, CancellationToken.None);
+            return SendAsync(
+                new WebSocketMessage<string>
+                {
+                    MessageId = Guid.NewGuid(),
+                    MessageType = "KeepAlive"
+                }, CancellationToken.None);
         }
 
         /// <inheritdoc />

+ 7 - 2
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -17,6 +17,8 @@ using Emby.Server.Implementations.Library.Resolvers;
 using Emby.Server.Implementations.Library.Validators;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.ScheduledTasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
@@ -25,7 +27,6 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
@@ -46,6 +47,9 @@ using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.MediaInfo;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
 using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
@@ -1539,7 +1543,8 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 // Handle grouping
-                if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
+                if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
                 {
                     return GetUserRootFolder()
                         .GetChildren(user, true)

+ 23 - 24
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -7,6 +7,8 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Entities;
@@ -14,7 +16,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding)
-                        {
-                            source.SupportsTranscoding = false;
-                        }
+                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
                     }
                 }
             }
@@ -352,7 +350,9 @@ namespace Emby.Server.Implementations.Library
 
         private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
         {
-            if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+            if (userData.SubtitleStreamIndex.HasValue
+                && user.RememberSubtitleSelections
+                && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
             {
                 var index = userData.SubtitleStreamIndex.Value;
                 // Make sure the saved index is still valid
@@ -363,26 +363,27 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
-                ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
+
+            var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
+                ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
 
             var defaultAudioIndex = source.DefaultAudioStreamIndex;
             var audioLangage = defaultAudioIndex == null
                 ? null
                 : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
 
-            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+            source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
+                source.MediaStreams,
                 preferredSubs,
-                user.Configuration.SubtitleMode,
+                user.SubtitleMode,
                 audioLangage);
 
-            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
-                user.Configuration.SubtitleMode, audioLangage);
+            MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
         }
 
         private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
         {
-            if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+            if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
             {
                 var index = userData.AudioStreamIndex.Value;
                 // Make sure the saved index is still valid
@@ -393,11 +394,11 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+            var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
                 ? Array.Empty<string>()
-                : NormalizeLanguage(user.Configuration.AudioLanguagePreference);
+                : NormalizeLanguage(user.AudioLanguagePreference);
 
-            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+            source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
         }
 
         public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
@@ -534,7 +535,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.RunTimeTicks = null;
             }
 
-            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+            var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 
             if (audioStream == null || audioStream.Index == -1)
             {
@@ -545,7 +546,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
             }
 
-            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
             if (videoStream != null)
             {
                 if (!videoStream.BitRate.HasValue)
@@ -556,17 +557,14 @@ namespace Emby.Server.Implementations.Library
                     {
                         videoStream.BitRate = 30000000;
                     }
-
                     else if (width >= 1900)
                     {
                         videoStream.BitRate = 20000000;
                     }
-
                     else if (width >= 1200)
                     {
                         videoStream.BitRate = 8000000;
                     }
-
                     else if (width >= 700)
                     {
                         videoStream.BitRate = 2000000;
@@ -670,13 +668,14 @@ namespace Emby.Server.Implementations.Library
                     mediaSource.AnalyzeDurationMs = 3000;
                 }
 
-                mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+                mediaInfo = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
                 {
                     MediaSource = mediaSource,
                     MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
                     ExtractChapters = false
-
-                }, cancellationToken).ConfigureAwait(false);
+                },
+                    cancellationToken).ConfigureAwait(false);
 
                 if (cacheFilePath != null)
                 {

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

@@ -3,7 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using MediaBrowser.Model.Configuration;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Server.Implementations.Library

+ 7 - 11
Emby.Server.Implementations/Library/MusicManager.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -10,6 +11,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library
                 {
                     return Guid.Empty;
                 }
-
             }).Where(i => !i.Equals(Guid.Empty)).ToArray();
 
             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
@@ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library
                 return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
             }
 
-            var playlist = item as Playlist;
-            if (playlist != null)
+            if (item is Playlist playlist)
             {
                 return GetInstantMixFromPlaylist(playlist, user, dtoOptions);
             }
 
-            var album = item as MusicAlbum;
-            if (album != null)
+            if (item is MusicAlbum album)
             {
                 return GetInstantMixFromAlbum(album, user, dtoOptions);
             }
 
-            var artist = item as MusicArtist;
-            if (artist != null)
+            if (item is MusicArtist artist)
             {
                 return GetInstantMixFromArtist(artist, user, dtoOptions);
             }
 
-            var song = item as Audio;
-            if (song != null)
+            if (item is Audio song)
             {
                 return GetInstantMixFromSong(song, user, dtoOptions);
             }
 
-            var folder = item as Folder;
-            if (folder != null)
+            if (item is Folder folder)
             {
                 return GetInstantMixFromFolder(folder, user, dtoOptions);
             }

+ 3 - 0
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -12,6 +13,8 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
 using Microsoft.Extensions.Logging;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
 
 namespace Emby.Server.Implementations.Library
 {

+ 2 - 0
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Threading;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
 
 namespace Emby.Server.Implementations.Library
 {

+ 0 - 1107
Emby.Server.Implementations/Library/UserManager.cs

@@ -1,1107 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Cryptography;
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Library
-{
-    /// <summary>
-    /// Class UserManager.
-    /// </summary>
-    public class UserManager : IUserManager
-    {
-        private readonly object _policySyncLock = new object();
-        private readonly object _configSyncLock = new object();
-
-        private readonly ILogger<UserManager> _logger;
-        private readonly IUserRepository _userRepository;
-        private readonly IXmlSerializer _xmlSerializer;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly INetworkManager _networkManager;
-        private readonly IImageProcessor _imageProcessor;
-        private readonly Lazy<IDtoService> _dtoServiceFactory;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IFileSystem _fileSystem;
-        private readonly ICryptoProvider _cryptoProvider;
-
-        private ConcurrentDictionary<Guid, User> _users;
-
-        private IAuthenticationProvider[] _authenticationProviders;
-        private DefaultAuthenticationProvider _defaultAuthenticationProvider;
-
-        private InvalidAuthProvider _invalidAuthProvider;
-
-        private IPasswordResetProvider[] _passwordResetProviders;
-        private DefaultPasswordResetProvider _defaultPasswordResetProvider;
-
-        private IDtoService DtoService => _dtoServiceFactory.Value;
-
-        public UserManager(
-            ILogger<UserManager> logger,
-            IUserRepository userRepository,
-            IXmlSerializer xmlSerializer,
-            INetworkManager networkManager,
-            IImageProcessor imageProcessor,
-            Lazy<IDtoService> dtoServiceFactory,
-            IServerApplicationHost appHost,
-            IJsonSerializer jsonSerializer,
-            IFileSystem fileSystem,
-            ICryptoProvider cryptoProvider)
-        {
-            _logger = logger;
-            _userRepository = userRepository;
-            _xmlSerializer = xmlSerializer;
-            _networkManager = networkManager;
-            _imageProcessor = imageProcessor;
-            _dtoServiceFactory = dtoServiceFactory;
-            _appHost = appHost;
-            _jsonSerializer = jsonSerializer;
-            _fileSystem = fileSystem;
-            _cryptoProvider = cryptoProvider;
-            _users = null;
-        }
-
-        public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
-
-        /// <summary>
-        /// Occurs when [user updated].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<User>> UserUpdated;
-
-        public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
-
-        public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
-
-        public event EventHandler<GenericEventArgs<User>> UserLockedOut;
-
-        public event EventHandler<GenericEventArgs<User>> UserCreated;
-
-        /// <summary>
-        /// Occurs when [user deleted].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<User>> UserDeleted;
-
-        /// <inheritdoc />
-        public IEnumerable<User> Users => _users.Values;
-
-        /// <inheritdoc />
-        public IEnumerable<Guid> UsersIds => _users.Keys;
-
-        /// <summary>
-        /// Called when [user updated].
-        /// </summary>
-        /// <param name="user">The user.</param>
-        private void OnUserUpdated(User user)
-        {
-            UserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        /// <summary>
-        /// Called when [user deleted].
-        /// </summary>
-        /// <param name="user">The user.</param>
-        private void OnUserDeleted(User user)
-        {
-            UserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        public NameIdPair[] GetAuthenticationProviders()
-        {
-            return _authenticationProviders
-                .Where(i => i.IsEnabled)
-                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
-                .ThenBy(i => i.Name)
-                .Select(i => new NameIdPair
-                {
-                    Name = i.Name,
-                    Id = GetAuthenticationProviderId(i)
-                })
-                .ToArray();
-        }
-
-        public NameIdPair[] GetPasswordResetProviders()
-        {
-            return _passwordResetProviders
-                .Where(i => i.IsEnabled)
-                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
-                .ThenBy(i => i.Name)
-                .Select(i => new NameIdPair
-                {
-                    Name = i.Name,
-                    Id = GetPasswordResetProviderId(i)
-                })
-                .ToArray();
-        }
-
-        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();
-        }
-
-        /// <inheritdoc />
-        public User GetUserById(Guid id)
-        {
-            if (id == Guid.Empty)
-            {
-                throw new ArgumentException("Guid can't be empty", nameof(id));
-            }
-
-            _users.TryGetValue(id, out User user);
-            return user;
-        }
-
-        public User GetUserByName(string name)
-        {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentException("Invalid username", nameof(name));
-            }
-
-            return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
-        }
-
-        public void Initialize()
-        {
-            LoadUsers();
-
-            var users = Users;
-
-            // If there are no local users with admin rights, make them all admins
-            if (!users.Any(i => i.Policy.IsAdministrator))
-            {
-                foreach (var user in users)
-                {
-                    user.Policy.IsAdministrator = true;
-                    UpdateUserPolicy(user, user.Policy, false);
-                }
-            }
-        }
-
-        public static bool IsValidUsername(string username)
-        {
-            // 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(username, @"^[\w\-'._@]*$");
-        }
-
-        private static bool IsValidUsernameCharacter(char i)
-            => IsValidUsername(i.ToString(CultureInfo.InvariantCulture));
-
-        public string MakeValidUsername(string username)
-        {
-            if (IsValidUsername(username))
-            {
-                return username;
-            }
-
-            // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
-            var builder = new StringBuilder();
-
-            foreach (var c in username)
-            {
-                if (IsValidUsernameCharacter(c))
-                {
-                    builder.Append(c);
-                }
-            }
-
-            return builder.ToString();
-        }
-
-        public async Task<User> AuthenticateUser(
-            string username,
-            string password,
-            string hashedPassword,
-            string remoteEndPoint,
-            bool isUserSession)
-        {
-            if (string.IsNullOrWhiteSpace(username))
-            {
-                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
-                throw new ArgumentNullException(nameof(username));
-            }
-
-            var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
-            var success = false;
-            IAuthenticationProvider authenticationProvider = null;
-
-            if (user != null)
-            {
-                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
-                authenticationProvider = authResult.authenticationProvider;
-                success = authResult.success;
-            }
-            else
-            {
-                // user is null
-                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
-                authenticationProvider = authResult.authenticationProvider;
-                string updatedUsername = authResult.username;
-                success = authResult.success;
-
-                if (success
-                    && authenticationProvider != null
-                    && !(authenticationProvider is DefaultAuthenticationProvider))
-                {
-                    // Trust the username returned by the authentication provider
-                    username = updatedUsername;
-
-                    // Search the database for the user again
-                    // the authentication provider might have created it
-                    user = Users
-                        .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
-                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
-                    {
-                        var policy = hasNewUserPolicy.GetNewUserPolicy();
-                        UpdateUserPolicy(user, policy, true);
-                    }
-                }
-            }
-
-            if (success && user != null && authenticationProvider != null)
-            {
-                var providerId = GetAuthenticationProviderId(authenticationProvider);
-
-                if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
-                {
-                    user.Policy.AuthenticationProviderId = providerId;
-                    UpdateUserPolicy(user, user.Policy, true);
-                }
-            }
-
-            if (user == null)
-            {
-                _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
-                throw new AuthenticationException("Invalid username or password entered.");
-            }
-
-            if (user.Policy.IsDisabled)
-            {
-                _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
-                throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator.");
-            }
-
-            if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
-            {
-                _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
-                throw new SecurityException("Forbidden.");
-            }
-
-            if (!user.IsParentalScheduleAllowed())
-            {
-                _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
-                throw new SecurityException("User is not allowed access at this time.");
-            }
-
-            // Update LastActivityDate and LastLoginDate, then save
-            if (success)
-            {
-                if (isUserSession)
-                {
-                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
-                    UpdateUser(user);
-                }
-
-                ResetInvalidLoginAttemptCount(user);
-                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
-            }
-            else
-            {
-                IncrementInvalidLoginAttemptCount(user);
-                _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
-            }
-
-            return success ? user : null;
-        }
-
-#nullable enable
-
-        private static string GetAuthenticationProviderId(IAuthenticationProvider provider)
-        {
-            return provider.GetType().FullName;
-        }
-
-        private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
-        {
-            return provider.GetType().FullName;
-        }
-
-        private IAuthenticationProvider GetAuthenticationProvider(User user)
-        {
-            return GetAuthenticationProviders(user)[0];
-        }
-
-        private IPasswordResetProvider GetPasswordResetProvider(User user)
-        {
-            return GetPasswordResetProviders(user)[0];
-        }
-
-        private IAuthenticationProvider[] GetAuthenticationProviders(User? user)
-        {
-            var authenticationProviderId = user?.Policy.AuthenticationProviderId;
-
-            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
-
-            if (!string.IsNullOrEmpty(authenticationProviderId))
-            {
-                providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
-            }
-
-            if (providers.Length == 0)
-            {
-                // 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?.Name, user?.Policy.AuthenticationProviderId);
-                providers = new IAuthenticationProvider[] { _invalidAuthProvider };
-            }
-
-            return providers;
-        }
-
-        private IPasswordResetProvider[] GetPasswordResetProviders(User? user)
-        {
-            var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
-
-            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
-
-            if (!string.IsNullOrEmpty(passwordResetProviderId))
-            {
-                providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
-            }
-
-            if (providers.Length == 0)
-            {
-                providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
-            }
-
-            return providers;
-        }
-
-        private async Task<(string username, bool success)> AuthenticateWithProvider(
-            IAuthenticationProvider provider,
-            string username,
-            string password,
-            User? resolvedUser)
-        {
-            try
-            {
-                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
-                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
-                    : await provider.Authenticate(username, password).ConfigureAwait(false);
-
-                if (authenticationResult.Username != username)
-                {
-                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
-                    username = authenticationResult.Username;
-                }
-
-                return (username, true);
-            }
-            catch (AuthenticationException ex)
-            {
-                _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
-
-                return (username, false);
-            }
-        }
-
-        private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
-            string username,
-            string password,
-            string hashedPassword,
-            User? user,
-            string remoteEndPoint)
-        {
-            bool success = false;
-            IAuthenticationProvider? authenticationProvider = null;
-
-            foreach (var provider in GetAuthenticationProviders(user))
-            {
-                var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
-                var updatedUsername = providerAuthResult.username;
-                success = providerAuthResult.success;
-
-                if (success)
-                {
-                    authenticationProvider = provider;
-                    username = updatedUsername;
-                    break;
-                }
-            }
-
-            if (!success
-                && _networkManager.IsInLocalNetwork(remoteEndPoint)
-                && user?.Configuration.EnableLocalPassword == true
-                && !string.IsNullOrEmpty(user.EasyPassword))
-            {
-                // Check easy password
-                var passwordHash = PasswordHash.Parse(user.EasyPassword);
-                var hash = _cryptoProvider.ComputeHash(
-                    passwordHash.Id,
-                    Encoding.UTF8.GetBytes(password),
-                    passwordHash.Salt.ToArray());
-                success = passwordHash.Hash.SequenceEqual(hash);
-            }
-
-            return (authenticationProvider, username, success);
-        }
-
-        private void ResetInvalidLoginAttemptCount(User user)
-        {
-            user.Policy.InvalidLoginAttemptCount = 0;
-            UpdateUserPolicy(user, user.Policy, false);
-        }
-
-        private void IncrementInvalidLoginAttemptCount(User user)
-        {
-            int invalidLogins = ++user.Policy.InvalidLoginAttemptCount;
-            int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout;
-            if (maxInvalidLogins > 0
-                && invalidLogins >= maxInvalidLogins)
-            {
-                user.Policy.IsDisabled = true;
-                UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
-                _logger.LogWarning(
-                    "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
-                    user.Name,
-                    invalidLogins);
-            }
-
-            UpdateUserPolicy(user, user.Policy, false);
-        }
-
-        /// <summary>
-        /// Loads the users from the repository.
-        /// </summary>
-        private void LoadUsers()
-        {
-            var users = _userRepository.RetrieveAllUsers();
-
-            // There always has to be at least one user.
-            if (users.Count != 0)
-            {
-                _users = new ConcurrentDictionary<Guid, User>(
-                    users.Select(x => new KeyValuePair<Guid, User>(x.Id, x)));
-                return;
-            }
-
-            var defaultName = Environment.UserName;
-            if (string.IsNullOrWhiteSpace(defaultName))
-            {
-                defaultName = "MyJellyfinUser";
-            }
-
-            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
-
-            var name = MakeValidUsername(defaultName);
-
-            var user = InstantiateNewUser(name);
-
-            user.DateLastSaved = DateTime.UtcNow;
-
-            _userRepository.CreateUser(user);
-
-            user.Policy.IsAdministrator = true;
-            user.Policy.EnableContentDeletion = true;
-            user.Policy.EnableRemoteControlOfOtherUsers = true;
-            UpdateUserPolicy(user, user.Policy, false);
-
-            _users = new ConcurrentDictionary<Guid, User>();
-            _users[user.Id] = user;
-        }
-
-#nullable restore
-
-        public UserDto GetUserDto(User user, string remoteEndPoint = null)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user);
-            bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
-
-            bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
-                hasConfiguredEasyPassword :
-                hasConfiguredPassword;
-
-            UserDto dto = new UserDto
-            {
-                Id = user.Id,
-                Name = user.Name,
-                HasPassword = hasPassword,
-                HasConfiguredPassword = hasConfiguredPassword,
-                HasConfiguredEasyPassword = hasConfiguredEasyPassword,
-                LastActivityDate = user.LastActivityDate,
-                LastLoginDate = user.LastLoginDate,
-                Configuration = user.Configuration,
-                ServerId = _appHost.SystemId,
-                Policy = user.Policy
-            };
-
-            if (!hasPassword && _users.Count == 1)
-            {
-                dto.EnableAutoLogin = true;
-            }
-
-            ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
-
-            if (image != null)
-            {
-                dto.PrimaryImageTag = GetImageCacheTag(user, image);
-
-                try
-                {
-                    DtoService.AttachPrimaryImageAspectRatio(dto, user);
-                }
-                catch (Exception ex)
-                {
-                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {User}", user.Name);
-                }
-            }
-
-            return dto;
-        }
-
-        public UserDto GetOfflineUserDto(User user)
-        {
-            var dto = GetUserDto(user);
-
-            dto.ServerName = _appHost.FriendlyName;
-
-            return dto;
-        }
-
-        private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
-        {
-            try
-            {
-                return _imageProcessor.GetImageCacheTag(item, image);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error getting {ImageType} image info for {ImagePath}", image.Type, image.Path);
-                return null;
-            }
-        }
-
-        /// <summary>
-        /// Refreshes metadata for each user
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task RefreshUsersMetadata(CancellationToken cancellationToken)
-        {
-            foreach (var user in Users)
-            {
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Renames the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="newName">The new name.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public async Task RenameUser(User user, string newName)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (string.IsNullOrWhiteSpace(newName))
-            {
-                throw new ArgumentException("Invalid username", nameof(newName));
-            }
-
-            if (user.Name.Equals(newName, StringComparison.Ordinal))
-            {
-                throw new ArgumentException("The new and old names must be different.");
-            }
-
-            if (Users.Any(
-                u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
-            {
-                throw new ArgumentException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "A user with the name '{0}' already exists.",
-                    newName));
-            }
-
-            await user.Rename(newName).ConfigureAwait(false);
-
-            OnUserUpdated(user);
-        }
-
-        /// <summary>
-        /// Updates the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <exception cref="ArgumentNullException">user</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public void UpdateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (user.Id == Guid.Empty)
-            {
-                throw new ArgumentException("Id can't be empty.", nameof(user));
-            }
-
-            if (!_users.ContainsKey(user.Id))
-            {
-                throw new ArgumentException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "A user '{0}' with Id {1} does not exist.",
-                        user.Name,
-                        user.Id),
-                    nameof(user));
-            }
-
-            user.DateModified = DateTime.UtcNow;
-            user.DateLastSaved = DateTime.UtcNow;
-
-            _userRepository.UpdateUser(user);
-
-            OnUserUpdated(user);
-        }
-
-        /// <summary>
-        /// Creates the user.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>User.</returns>
-        /// <exception cref="ArgumentNullException">name</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public User CreateUser(string name)
-        {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            if (!IsValidUsername(name))
-            {
-                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
-            }
-
-            if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
-            {
-                throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
-            }
-
-            var user = InstantiateNewUser(name);
-
-            _users[user.Id] = user;
-
-            user.DateLastSaved = DateTime.UtcNow;
-
-            _userRepository.CreateUser(user);
-
-            EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User>(user), _logger);
-
-            return user;
-        }
-
-        /// <inheritdoc />
-        /// <exception cref="ArgumentNullException">The <c>user</c> is <c>null</c>.</exception>
-        /// <exception cref="ArgumentException">The <c>user</c> doesn't exist, or is the last administrator.</exception>
-        /// <exception cref="InvalidOperationException">The <c>user</c> can't be deleted; there are no other users.</exception>
-        public void DeleteUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (!_users.ContainsKey(user.Id))
-            {
-                throw new ArgumentException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
-                    user.Name,
-                    user.Id));
-            }
-
-            if (_users.Count == 1)
-            {
-                throw new InvalidOperationException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "The user '{0}' cannot be deleted because there must be at least one user in the system.",
-                    user.Name));
-            }
-
-            if (user.Policy.IsAdministrator
-                && Users.Count(i => i.Policy.IsAdministrator) == 1)
-            {
-                throw new ArgumentException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
-                        user.Name),
-                    nameof(user));
-            }
-
-            var configPath = GetConfigurationFilePath(user);
-
-            _userRepository.DeleteUser(user);
-
-            // Delete user config dir
-            lock (_configSyncLock)
-                lock (_policySyncLock)
-                {
-                    try
-                    {
-                        Directory.Delete(user.ConfigurationDirectoryPath, true);
-                    }
-                    catch (IOException ex)
-                    {
-                        _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath);
-                    }
-                }
-
-            _users.TryRemove(user.Id, out _);
-
-            OnUserDeleted(user);
-        }
-
-        /// <summary>
-        /// Resets the password by clearing it.
-        /// </summary>
-        /// <returns>Task.</returns>
-        public Task ResetPassword(User user)
-        {
-            return ChangePassword(user, string.Empty);
-        }
-
-        public void ResetEasyPassword(User user)
-        {
-            ChangeEasyPassword(user, string.Empty, null);
-        }
-
-        public async Task ChangePassword(User user, string newPassword)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
-
-            UpdateUser(user);
-
-            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
-
-            UpdateUser(user);
-
-            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        /// <summary>
-        /// Instantiates the new user.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>User.</returns>
-        private static User InstantiateNewUser(string name)
-        {
-            return new User
-            {
-                Name = name,
-                Id = Guid.NewGuid(),
-                DateCreated = DateTime.UtcNow,
-                DateModified = DateTime.UtcNow
-            };
-        }
-
-        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
-        {
-            var user = string.IsNullOrWhiteSpace(enteredUsername) ?
-                null :
-                GetUserByName(enteredUsername);
-
-            var action = ForgotPasswordAction.InNetworkRequired;
-
-            if (user != null && isInNetwork)
-            {
-                var passwordResetProvider = GetPasswordResetProvider(user);
-                return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
-            }
-            else
-            {
-                return new ForgotPasswordResult
-                {
-                    Action = action,
-                    PinFile = string.Empty
-                };
-            }
-        }
-
-        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
-        {
-            foreach (var provider in _passwordResetProviders)
-            {
-                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
-                if (result.Success)
-                {
-                    return result;
-                }
-            }
-
-            return new PinRedeemResult
-            {
-                Success = false,
-                UsersReset = Array.Empty<string>()
-            };
-        }
-
-        public UserPolicy GetUserPolicy(User user)
-        {
-            var path = GetPolicyFilePath(user);
-            if (!File.Exists(path))
-            {
-                return GetDefaultPolicy();
-            }
-
-            try
-            {
-                lock (_policySyncLock)
-                {
-                    return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
-                return GetDefaultPolicy();
-            }
-        }
-
-        private static UserPolicy GetDefaultPolicy()
-        {
-            return new UserPolicy
-            {
-                EnableContentDownloading = true,
-                EnableSyncTranscoding = true
-            };
-        }
-
-        public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
-        {
-            var user = GetUserById(userId);
-            UpdateUserPolicy(user, userPolicy, true);
-        }
-
-        private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent)
-        {
-            // The xml serializer will output differently if the type is not exact
-            if (userPolicy.GetType() != typeof(UserPolicy))
-            {
-                var json = _jsonSerializer.SerializeToString(userPolicy);
-                userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
-            }
-
-            var path = GetPolicyFilePath(user);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            lock (_policySyncLock)
-            {
-                _xmlSerializer.SerializeToFile(userPolicy, path);
-                user.Policy = userPolicy;
-            }
-
-            if (fireEvent)
-            {
-                UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User>(user));
-            }
-        }
-
-        private static string GetPolicyFilePath(User user)
-        {
-            return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
-        }
-
-        private static string GetConfigurationFilePath(User user)
-        {
-            return Path.Combine(user.ConfigurationDirectoryPath, "config.xml");
-        }
-
-        public UserConfiguration GetUserConfiguration(User user)
-        {
-            var path = GetConfigurationFilePath(user);
-
-            if (!File.Exists(path))
-            {
-                return new UserConfiguration();
-            }
-
-            try
-            {
-                lock (_configSyncLock)
-                {
-                    return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
-                return new UserConfiguration();
-            }
-        }
-
-        public void UpdateConfiguration(Guid userId, UserConfiguration config)
-        {
-            var user = GetUserById(userId);
-            UpdateConfiguration(user, config);
-        }
-
-        public void UpdateConfiguration(User user, UserConfiguration config)
-        {
-            UpdateConfiguration(user, config, true);
-        }
-
-        private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent)
-        {
-            var path = GetConfigurationFilePath(user);
-
-            // The xml serializer will output differently if the type is not exact
-            if (config.GetType() != typeof(UserConfiguration))
-            {
-                var json = _jsonSerializer.SerializeToString(config);
-                config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json);
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            lock (_configSyncLock)
-            {
-                _xmlSerializer.SerializeToFile(config, path);
-                user.Configuration = config;
-            }
-
-            if (fireEvent)
-            {
-                UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User>(user));
-            }
-        }
-    }
-
-    public class DeviceAccessEntryPoint : IServerEntryPoint
-    {
-        private IUserManager _userManager;
-        private IAuthenticationRepository _authRepo;
-        private IDeviceManager _deviceManager;
-        private ISessionManager _sessionManager;
-
-        public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
-        {
-            _userManager = userManager;
-            _authRepo = authRepo;
-            _deviceManager = deviceManager;
-            _sessionManager = sessionManager;
-        }
-
-        public Task RunAsync()
-        {
-            _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
-
-            return Task.CompletedTask;
-        }
-
-        private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var user = e.Argument;
-            if (!user.Policy.EnableAllDevices)
-            {
-                UpdateDeviceAccess(user);
-            }
-        }
-
-        private void UpdateDeviceAccess(User user)
-        {
-            var existing = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                UserId = user.Id
-
-            }).Items;
-
-            foreach (var authInfo in existing)
-            {
-                if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
-                {
-                    _sessionManager.Logout(authInfo);
-                }
-            }
-        }
-
-        public void Dispose()
-        {
-
-        }
-    }
-}

+ 20 - 10
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -5,6 +5,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -17,6 +19,8 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library
 
             if (!query.IncludeHidden)
             {
-                list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
+                list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
             }
 
             var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
 
-            var orders = user.Configuration.OrderedViews.ToList();
+            var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
 
             return list
                 .OrderBy(i =>
@@ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library
             return GetUserSubViewWithName(name, parentId, type, sortName);
         }
 
-        private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews)
+        private Folder GetUserView(
+            List<ICollectionFolder> parents,
+            string viewType,
+            string localizationKey,
+            string sortName,
+            Jellyfin.Data.Entities.User user,
+            string[] presetViews)
         {
             if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
             {
@@ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library
             {
                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
                     .Where(i => i is Folder)
-                    .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+                    .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+                        .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
                     .ToList();
             }
 
@@ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library
 
             var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
             {
-                typeof(Person).Name,
-                typeof(Studio).Name,
-                typeof(Year).Name,
-                typeof(MusicGenre).Name,
-                typeof(Genre).Name
-
+                nameof(Person),
+                nameof(Studio),
+                nameof(Year),
+                nameof(MusicGenre),
+                nameof(Genre)
             } : Array.Empty<string>();
 
             var query = new InternalItemsQuery(user)

+ 12 - 13
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -7,6 +7,8 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
@@ -14,8 +16,6 @@ using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
@@ -31,6 +31,8 @@ using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 
 namespace Emby.Server.Implementations.LiveTv
 {
@@ -696,7 +698,6 @@ namespace Emby.Server.Implementations.LiveTv
                     {
                         Path = info.ThumbImageUrl,
                         Type = ImageType.Thumb
-
                     }, 0);
                 }
             }
@@ -709,7 +710,6 @@ namespace Emby.Server.Implementations.LiveTv
                     {
                         Path = info.LogoImageUrl,
                         Type = ImageType.Logo
-
                     }, 0);
                 }
             }
@@ -722,7 +722,6 @@ namespace Emby.Server.Implementations.LiveTv
                     {
                         Path = info.BackdropImageUrl,
                         Type = ImageType.Backdrop
-
                     }, 0);
                 }
             }
@@ -760,7 +759,8 @@ namespace Emby.Server.Implementations.LiveTv
 
             var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
 
-            var list = new List<Tuple<BaseItemDto, string, string>>() {
+            var list = new List<Tuple<BaseItemDto, string, string>>
+            {
                 new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId)
             };
 
@@ -2167,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv
             var info = new LiveTvInfo
             {
                 Services = services,
-                IsEnabled = services.Length > 0
+                IsEnabled = services.Length > 0,
+                EnabledUsers = _userManager.Users
+                    .Where(IsLiveTvEnabled)
+                    .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+                    .ToArray()
             };
 
-            info.EnabledUsers = _userManager.Users
-                .Where(IsLiveTvEnabled)
-                .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
-                .ToArray();
-
             return info;
         }
 
         private bool IsLiveTvEnabled(User user)
         {
-            return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
+            return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
         }
 
         public IEnumerable<User> GetEnabledUsers()

+ 2 - 1
Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs

@@ -3,6 +3,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Querying;
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists
             }
 
             query.Recursive = true;
-            query.IncludeItemTypes = new string[] { "Playlist" };
+            query.IncludeItemTypes = new[] { "Playlist" };
             query.Parent = null;
             return LibraryManager.GetItemsResult(query);
         }

+ 3 - 0
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -21,6 +22,8 @@ using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using PlaylistsNET.Content;
 using PlaylistsNET.Models;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
 namespace Emby.Server.Implementations.Playlists
 {

+ 52 - 30
Emby.Server.Implementations/Session/SessionManager.cs

@@ -7,6 +7,8 @@ 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;
 using MediaBrowser.Controller;
@@ -15,7 +17,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;
@@ -28,7 +29,9 @@ using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.SyncPlay;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 
 namespace Emby.Server.Implementations.Session
 {
@@ -283,11 +286,18 @@ namespace Emby.Server.Implementations.Session
             if (user != null)
             {
                 var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
-                user.LastActivityDate = activityDate;
 
                 if ((activityDate - userLastActivityDate).TotalSeconds > 60)
                 {
-                    _userManager.UpdateUser(user);
+                    try
+                    {
+                        user.LastActivityDate = activityDate;
+                        _userManager.UpdateUser(user);
+                    }
+                    catch (DbUpdateConcurrencyException e)
+                    {
+                        _logger.LogWarning(e, "Error updating user's last activity date.");
+                    }
                 }
             }
 
@@ -434,7 +444,13 @@ namespace Emby.Server.Implementations.Session
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <param name="user">The user.</param>
         /// <returns>SessionInfo.</returns>
-        private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+        private SessionInfo GetSessionInfo(
+            string appName,
+            string appVersion,
+            string deviceId,
+            string deviceName,
+            string remoteEndPoint,
+            User user)
         {
             CheckDisposed();
 
@@ -447,14 +463,13 @@ namespace Emby.Server.Implementations.Session
 
             CheckDisposed();
 
-            var sessionInfo = _activeConnections.GetOrAdd(key, k =>
-            {
-                return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
-            });
+            var sessionInfo = _activeConnections.GetOrAdd(
+                key,
+                k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
 
-            sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
-            sessionInfo.UserName = user?.Name;
-            sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+            sessionInfo.UserId = user?.Id ?? Guid.Empty;
+            sessionInfo.UserName = user?.Username;
+            sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
             sessionInfo.RemoteEndPoint = remoteEndPoint;
             sessionInfo.Client = appName;
 
@@ -473,7 +488,14 @@ namespace Emby.Server.Implementations.Session
             return sessionInfo;
         }
 
-        private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+        private SessionInfo CreateSession(
+            string key,
+            string appName,
+            string appVersion,
+            string deviceId,
+            string deviceName,
+            string remoteEndPoint,
+            User user)
         {
             var sessionInfo = new SessionInfo(this, _logger)
             {
@@ -483,11 +505,11 @@ namespace Emby.Server.Implementations.Session
                 Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
-            var username = user?.Name;
+            var username = user?.Username;
 
             sessionInfo.UserId = user?.Id ?? Guid.Empty;
             sessionInfo.UserName = username;
-            sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+            sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
             sessionInfo.RemoteEndPoint = remoteEndPoint;
 
             if (string.IsNullOrEmpty(deviceName))
@@ -535,10 +557,7 @@ namespace Emby.Server.Implementations.Session
 
         private void StartIdleCheckTimer()
         {
-            if (_idleTimer == null)
-            {
-                _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
-            }
+            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
         }
 
         private void StopIdleCheckTimer()
@@ -786,7 +805,7 @@ namespace Emby.Server.Implementations.Session
         {
             var changed = false;
 
-            if (user.Configuration.RememberAudioSelections)
+            if (user.RememberAudioSelections)
             {
                 if (data.AudioStreamIndex != info.AudioStreamIndex)
                 {
@@ -803,7 +822,7 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            if (user.Configuration.RememberSubtitleSelections)
+            if (user.RememberSubtitleSelections)
             {
                 if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
                 {
@@ -1114,13 +1133,13 @@ namespace Emby.Server.Implementations.Session
                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
                 {
                     throw new ArgumentException(
-                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
+                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
                 }
             }
 
             if (user != null
                 && command.ItemIds.Length == 1
-                && user.Configuration.EnableNextEpisodeAutoPlay
+                && user.EnableNextEpisodeAutoPlay
                 && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
             {
                 var series = episode.Series;
@@ -1191,7 +1210,7 @@ namespace Emby.Server.Implementations.Session
                     DtoOptions = new DtoOptions(false)
                     {
                         EnableImages = false,
-                        Fields = new ItemFields[]
+                        Fields = new[]
                         {
                             ItemFields.SortName
                         }
@@ -1353,7 +1372,7 @@ namespace Emby.Server.Implementations.Session
                 list.Add(new SessionUserInfo
                 {
                     UserId = userId,
-                    UserName = user.Name
+                    UserName = user.Username
                 });
 
                 session.AdditionalUsers = list.ToArray();
@@ -1513,7 +1532,7 @@ namespace Emby.Server.Implementations.Session
                 DeviceName = deviceName,
                 UserId = user.Id,
                 AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
-                UserName = user.Name
+                UserName = user.Username
             };
 
             _logger.LogInformation("Creating new access token for user {0}", user.Id);
@@ -1710,15 +1729,15 @@ namespace Emby.Server.Implementations.Session
             return info;
         }
 
-        private string GetImageCacheTag(BaseItem item, ImageType type)
+        private string GetImageCacheTag(User user)
         {
             try
             {
-                return _imageProcessor.GetImageCacheTag(item, type);
+                return _imageProcessor.GetImageCacheTag(user);
             }
-            catch (Exception ex)
+            catch (Exception e)
             {
-                _logger.LogError(ex, "Error getting image information for {Type}", type);
+                _logger.LogError(e, "Error getting image information for profile image");
                 return null;
             }
         }
@@ -1827,7 +1846,10 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
 
-            var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList();
+            var adminUserIds = _userManager.Users
+                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
+                .Select(i => i.Id)
+                .ToList();
 
             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
         }

+ 1 - 0
Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 1 - 0
Emby.Server.Implementations/Sorting/DatePlayedComparer.cs

@@ -1,4 +1,5 @@
 using System;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 1 - 0
Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 1 - 0
Emby.Server.Implementations/Sorting/IsPlayedComparer.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 1 - 0
Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 1 - 0
Emby.Server.Implementations/Sorting/PlayCountComparer.cs

@@ -1,3 +1,4 @@
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Sorting;

+ 27 - 52
Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs

@@ -3,13 +3,13 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
-using Microsoft.Extensions.Logging;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.SyncPlay
 {
@@ -109,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay
             _disposed = true;
         }
 
-        private void CheckDisposed()
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
-        }
-
         private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
         {
             var session = e.SessionInfo;
@@ -149,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay
             var item = _libraryManager.GetItemById(itemId);
 
             // Check ParentalRating access
-            var hasParentalRatingAccess = true;
-            if (user.Policy.MaxParentalRating.HasValue)
-            {
-                hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
-            }
+            var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
+                || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
 
-            if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
+            if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
             {
                 var collections = _libraryManager.GetCollectionFolders(item).Select(
-                    folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
-                );
-                var intersect = collections.Intersect(user.Policy.EnabledFolders);
-                return intersect.Any();
-            }
-            else
-            {
-                return hasParentalRatingAccess;
+                    folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
+
+                return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
             }
+
+            return hasParentalRatingAccess;
         }
 
         private Guid? GetSessionGroup(SessionInfo session)
         {
-            ISyncPlayController group;
-            _sessionToGroupMap.TryGetValue(session.Id, out group);
-            if (group != null)
-            {
-                return group.GetGroupId();
-            }
-            else
-            {
-                return null;
-            }
+            _sessionToGroupMap.TryGetValue(session.Id, out var group);
+            return group?.GetGroupId();
         }
 
         /// <inheritdoc />
@@ -188,7 +166,7 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             var user = _userManager.GetUserById(session.UserId);
 
-            if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
+            if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
             {
                 _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
 
@@ -196,7 +174,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 {
                     Type = GroupUpdateType.CreateGroupDenied
                 };
-                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
 
@@ -219,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             var user = _userManager.GetUserById(session.UserId);
 
-            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            if (user.SyncPlayAccess == SyncPlayAccess.None)
             {
                 _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
 
@@ -227,7 +205,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 {
                     Type = GroupUpdateType.JoinGroupDenied
                 };
-                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
 
@@ -244,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
                     {
                         Type = GroupUpdateType.GroupDoesNotExist
                     };
-                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                     return;
                 }
 
@@ -257,7 +235,7 @@ namespace Emby.Server.Implementations.SyncPlay
                         GroupId = group.GetGroupId().ToString(),
                         Type = GroupUpdateType.LibraryAccessDenied
                     };
-                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                     return;
                 }
 
@@ -281,8 +259,7 @@ namespace Emby.Server.Implementations.SyncPlay
             // TODO: determine what happens to users that are in a group and get their permissions revoked
             lock (_groupsLock)
             {
-                ISyncPlayController group;
-                _sessionToGroupMap.TryGetValue(session.Id, out group);
+                _sessionToGroupMap.TryGetValue(session.Id, out var group);
 
                 if (group == null)
                 {
@@ -292,7 +269,7 @@ namespace Emby.Server.Implementations.SyncPlay
                     {
                         Type = GroupUpdateType.NotInGroup
                     };
-                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                     return;
                 }
 
@@ -311,7 +288,7 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             var user = _userManager.GetUserById(session.UserId);
 
-            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            if (user.SyncPlayAccess == SyncPlayAccess.None)
             {
                 return new List<GroupInfoView>();
             }
@@ -341,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay
         {
             var user = _userManager.GetUserById(session.UserId);
 
-            if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+            if (user.SyncPlayAccess == SyncPlayAccess.None)
             {
                 _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
 
@@ -349,14 +326,13 @@ namespace Emby.Server.Implementations.SyncPlay
                 {
                     Type = GroupUpdateType.JoinGroupDenied
                 };
-                _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
 
             lock (_groupsLock)
             {
-                ISyncPlayController group;
-                _sessionToGroupMap.TryGetValue(session.Id, out group);
+                _sessionToGroupMap.TryGetValue(session.Id, out var group);
 
                 if (group == null)
                 {
@@ -366,7 +342,7 @@ namespace Emby.Server.Implementations.SyncPlay
                     {
                         Type = GroupUpdateType.NotInGroup
                     };
-                    _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                     return;
                 }
 
@@ -393,8 +369,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 throw new InvalidOperationException("Session not in any group!");
             }
 
-            ISyncPlayController tempGroup;
-            _sessionToGroupMap.Remove(session.Id, out tempGroup);
+            _sessionToGroupMap.Remove(session.Id, out var tempGroup);
 
             if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
             {

+ 9 - 5
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -4,13 +4,17 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Server.Implementations.TV
 {
@@ -73,7 +77,8 @@ namespace Emby.Server.Implementations.TV
             {
                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
                    .Where(i => i is Folder)
-                   .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+                   .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+                       .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
                    .ToArray();
             }
 
@@ -191,7 +196,7 @@ namespace Emby.Server.Implementations.TV
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
-                IncludeItemTypes = new[] { typeof(Episode).Name },
+                IncludeItemTypes = new[] { nameof(Episode) },
                 OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
                 IsPlayed = true,
                 Limit = 1,
@@ -204,7 +209,6 @@ namespace Emby.Server.Implementations.TV
                     },
                     EnableImages = false
                 }
-
             }).FirstOrDefault();
 
             Func<Episode> getEpisode = () =>
@@ -219,7 +223,7 @@ namespace Emby.Server.Implementations.TV
                     IsPlayed = false,
                     IsVirtualItem = false,
                     ParentIndexNumberNotEquals = 0,
-                    MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName,
+                    MinSortName = lastWatchedEpisode?.SortName,
                     DtoOptions = dtoOptions
 
                 }).Cast<Episode>().FirstOrDefault();

+ 3 - 2
Jellyfin.Api/Auth/CustomAuthenticationHandler.cs

@@ -3,6 +3,7 @@ using System.Security.Claims;
 using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.Extensions.Logging;
@@ -56,10 +57,10 @@ namespace Jellyfin.Api.Auth
 
                 var claims = new[]
                 {
-                    new Claim(ClaimTypes.Name, user.Name),
+                    new Claim(ClaimTypes.Name, user.Username),
                     new Claim(
                         ClaimTypes.Role,
-                        value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User)
+                        value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
                 };
                 var identity = new ClaimsIdentity(claims, Scheme.Name);
                 var principal = new ClaimsPrincipal(identity);

+ 9 - 3
Jellyfin.Api/Controllers/StartupController.cs

@@ -113,8 +113,14 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<StartupUserDto> GetFirstUser()
         {
+            // TODO: Remove this method when startup wizard no longer requires an existing user.
+            _userManager.Initialize();
             var user = _userManager.Users.First();
-            return new StartupUserDto { Name = user.Name, Password = user.Password };
+            return new StartupUserDto
+            {
+                Name = user.Username,
+                Password = user.Password
+            };
         }
 
         /// <summary>
@@ -132,9 +138,9 @@ namespace Jellyfin.Api.Controllers
         {
             var user = _userManager.Users.First();
 
-            user.Name = startupUserDto.Name;
+            user.Username = startupUserDto.Name;
 
-            _userManager.UpdateUser(user);
+            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
             if (!string.IsNullOrEmpty(startupUserDto.Password))
             {

+ 65 - 0
Jellyfin.Data/DayOfWeekHelper.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data
+{
+    public static class DayOfWeekHelper
+    {
+        public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+        {
+            var days = new List<DayOfWeek>(7);
+
+            if (day == DynamicDayOfWeek.Sunday
+                || day == DynamicDayOfWeek.Weekend
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Sunday);
+            }
+
+            if (day == DynamicDayOfWeek.Monday
+                || day == DynamicDayOfWeek.Weekday
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Monday);
+            }
+
+            if (day == DynamicDayOfWeek.Tuesday
+                || day == DynamicDayOfWeek.Weekday
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Tuesday);
+            }
+
+            if (day == DynamicDayOfWeek.Wednesday
+                || day == DynamicDayOfWeek.Weekday
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Wednesday);
+            }
+
+            if (day == DynamicDayOfWeek.Thursday
+                || day == DynamicDayOfWeek.Weekday
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Thursday);
+            }
+
+            if (day == DynamicDayOfWeek.Friday
+                || day == DynamicDayOfWeek.Weekday
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Friday);
+            }
+
+            if (day == DynamicDayOfWeek.Saturday
+                || day == DynamicDayOfWeek.Weekend
+                || day == DynamicDayOfWeek.Everyday)
+            {
+                days.Add(DayOfWeek.Saturday);
+            }
+
+            return days;
+        }
+    }
+}

+ 91 - 0
Jellyfin.Data/Entities/AccessSchedule.cs

@@ -0,0 +1,91 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using System.Xml.Serialization;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    /// <summary>
+    /// An entity representing a user's access schedule.
+    /// </summary>
+    public class AccessSchedule
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
+        /// </summary>
+        /// <param name="dayOfWeek">The day of the week.</param>
+        /// <param name="startHour">The start hour.</param>
+        /// <param name="endHour">The end hour.</param>
+        /// <param name="userId">The associated user's id.</param>
+        public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
+        {
+            UserId = userId;
+            DayOfWeek = dayOfWeek;
+            StartHour = startHour;
+            EndHour = endHour;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected AccessSchedule()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the id of this instance.
+        /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
+        [XmlIgnore]
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the id of the associated user.
+        /// </summary>
+        [XmlIgnore]
+        [Required]
+        public Guid UserId { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the day of week.
+        /// </summary>
+        /// <value>The day of week.</value>
+        [Required]
+        public DynamicDayOfWeek DayOfWeek { get; set; }
+
+        /// <summary>
+        /// Gets or sets the start hour.
+        /// </summary>
+        /// <value>The start hour.</value>
+        [Required]
+        public double StartHour { get; set; }
+
+        /// <summary>
+        /// Gets or sets the end hour.
+        /// </summary>
+        /// <value>The end hour.</value>
+        [Required]
+        public double EndHour { get; set; }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="dayOfWeek">The day of the week.</param>
+        /// <param name="startHour">The start hour.</param>
+        /// <param name="endHour">The end hour.</param>
+        /// <param name="userId">The associated user's id.</param>
+        /// <returns>The newly created instance.</returns>
+        public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
+        {
+            return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
+        }
+    }
+}

+ 58 - 45
Jellyfin.Data/Entities/Group.cs

@@ -2,19 +2,32 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
-    public partial class Group
+    /// <summary>
+    /// An entity representing a group.
+    /// </summary>
+    public partial class Group : IHasPermissions, ISavingChanges
     {
-        partial void Init();
-
         /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// Initializes a new instance of the <see cref="Group"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        protected Group()
+        /// <param name="name">The name of the group.</param>
+        public Group(string name)
         {
-            GroupPermissions = new HashSet<Permission>();
+            if (string.IsNullOrEmpty(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            Name = name;
+            Id = Guid.NewGuid();
+
+            Permissions = new HashSet<Permission>();
             ProviderMappings = new HashSet<ProviderMapping>();
             Preferences = new HashSet<Preference>();
 
@@ -22,66 +35,45 @@ namespace Jellyfin.Data.Entities
         }
 
         /// <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
+        /// Initializes a new instance of the <see cref="Group"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
-        /// <param name="name"></param>
-        /// <param name="_user0"></param>
-        public Group(string name, User _user0)
+        protected Group()
         {
-            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
-            this.Name = name;
-
-            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
-            _user0.Groups.Add(this);
-
-            this.GroupPermissions = new HashSet<Permission>();
-            this.ProviderMappings = new HashSet<ProviderMapping>();
-            this.Preferences = new HashSet<Preference>();
-
             Init();
         }
 
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="name"></param>
-        /// <param name="_user0"></param>
-        public static Group Create(string name, User _user0)
-        {
-            return new Group(name, _user0);
-        }
-
         /*************************************************************************
          * Properties
          *************************************************************************/
 
         /// <summary>
-        /// Identity, Indexed, Required
+        /// Gets or sets the id of this group.
         /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
         [Key]
         [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id { get; protected set; }
+        public Guid Id { get; protected set; }
 
         /// <summary>
-        /// Required, Max length = 255
+        /// Gets or sets the group's name.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 255.
+        /// </remarks>
         [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string Name { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
         /// </summary>
+        /// <remarks>
+        /// Required, Concurrency Token.
+        /// </remarks>
         [ConcurrencyCheck]
         [Required]
         public uint RowVersion { get; set; }
@@ -96,7 +88,7 @@ namespace Jellyfin.Data.Entities
          *************************************************************************/
 
         [ForeignKey("Permission_GroupPermissions_Id")]
-        public virtual ICollection<Permission> GroupPermissions { get; protected set; }
+        public virtual ICollection<Permission> Permissions { get; protected set; }
 
         [ForeignKey("ProviderMapping_ProviderMappings_Id")]
         public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
@@ -104,6 +96,27 @@ namespace Jellyfin.Data.Entities
         [ForeignKey("Preference_Preferences_Id")]
         public virtual ICollection<Preference> Preferences { get; protected set; }
 
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="name">The name of this group</param>
+        public static Group Create(string name)
+        {
+            return new Group(name);
+        }
+
+        /// <inheritdoc/>
+        public bool HasPermission(PermissionKind kind)
+        {
+            return Permissions.First(p => p.Kind == kind).Value;
+        }
+
+        /// <inheritdoc/>
+        public void SetPermission(PermissionKind kind, bool value)
+        {
+            Permissions.First(p => p.Kind == kind).Value = value;
+        }
+
+        partial void Init();
     }
 }
-

+ 30 - 0
Jellyfin.Data/Entities/ImageInfo.cs

@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public class ImageInfo
+    {
+        public ImageInfo(string path)
+        {
+            Path = path;
+            LastModified = DateTime.UtcNow;
+        }
+
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        public Guid? UserId { get; protected set; }
+
+        [Required]
+        [MaxLength(512)]
+        [StringLength(512)]
+        public string Path { get; set; }
+
+        [Required]
+        public DateTime LastModified { get; set; }
+    }
+}

+ 45 - 92
Jellyfin.Data/Entities/Permission.cs

@@ -1,64 +1,35 @@
-using System;
-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
+    /// <summary>
+    /// An entity representing whether the associated user has a specific permission.
+    /// </summary>
+    public partial class Permission : ISavingChanges
     {
-        partial void Init();
-
-        /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Permission()
-        {
-            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="Permission"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        public static Permission CreatePermissionUnsafe()
+        /// <param name="kind">The permission kind.</param>
+        /// <param name="value">The value of this permission.</param>
+        public Permission(PermissionKind kind, bool value)
         {
-            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)
-        {
-            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();
         }
 
         /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
+        /// Initializes a new instance of the <see cref="Permission"/> 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 Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+        protected Permission()
         {
-            return new Permission(kind, value, _user0, _group1);
+            Init();
         }
 
         /*************************************************************************
@@ -66,79 +37,61 @@ namespace Jellyfin.Data.Entities
          *************************************************************************/
 
         /// <summary>
-        /// Identity, Indexed, Required
+        /// Gets or sets the id of this permission.
         /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
         [Key]
         [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
         /// <summary>
-        /// Backing field for Kind
-        /// </summary>
-        protected Enums.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);
-        /// <summary>
-        /// When provided in a partial class, allows value of Kind to be changed before returning.
-        /// </summary>
-        partial void GetKind(ref Enums.PermissionKind result);
-
-        /// <summary>
-        /// Required
+        /// Gets or sets the type of this permission.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
-        public Enums.PermissionKind Kind
-        {
-            get
-            {
-                Enums.PermissionKind value = _Kind;
-                GetKind(ref value);
-                return (_Kind = value);
-            }
-            set
-            {
-                Enums.PermissionKind oldValue = _Kind;
-                SetKind(oldValue, ref value);
-                if (oldValue != value)
-                {
-                    _Kind = value;
-                    OnPropertyChanged();
-                }
-            }
-        }
+        public PermissionKind Kind { get; protected set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets a value indicating whether the associated user has this permission.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool Value { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
         /// </summary>
+        /// <remarks>
+        /// Required, ConcurrencyToken.
+        /// </remarks>
         [ConcurrencyCheck]
         [Required]
         public uint RowVersion { get; set; }
 
-        public void OnSavingChanges()
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="kind">The permission kind.</param>
+        /// <param name="value">The value of this permission.</param>
+        /// <returns>The newly created instance.</returns>
+        public static Permission Create(PermissionKind kind, bool value)
         {
-            RowVersion++;
+            return new Permission(kind, value);
         }
 
-        /*************************************************************************
-         * Navigation properties
-         *************************************************************************/
-
-        public virtual event PropertyChangedEventHandler PropertyChanged;
-
-        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        /// <inheritdoc/>
+        public void OnSavingChanges()
         {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+            RowVersion++;
         }
 
+        partial void Init();
     }
 }
-

+ 44 - 56
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();
-        }
-
-        /// <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();
+            Kind = kind;
+            Value = value ?? throw new ArgumentNullException(nameof(value));
         }
 
         /// <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);
         }
 
         /*************************************************************************
@@ -65,43 +35,61 @@ namespace Jellyfin.Data.Entities
          *************************************************************************/
 
         /// <summary>
-        /// Identity, Indexed, Required
+        /// Gets or sets the id of this preference.
         /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
         [Key]
         [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the type of this preference.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
-        public Enums.PreferenceKind Kind { get; set; }
+        public PreferenceKind Kind { get; protected set; }
 
         /// <summary>
-        /// Required, Max length = 65535
+        /// Gets or sets the value of this preference.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 65535.
+        /// </remarks>
         [Required]
         [MaxLength(65535)]
         [StringLength(65535)]
         public string Value { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
         /// </summary>
+        /// <remarks>
+        /// Required, ConcurrencyToken.
+        /// </remarks>
         [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
-         *************************************************************************/
-
     }
 }
-

+ 0 - 6
Jellyfin.Data/Entities/ProviderMapping.cs

@@ -43,12 +43,6 @@ namespace Jellyfin.Data.Entities
             if (string.IsNullOrEmpty(providerdata)) throw new ArgumentNullException(nameof(providerdata));
             this.ProviderData = providerdata;
 
-            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
-            _user0.ProviderMappings.Add(this);
-
-            if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
-            _group1.ProviderMappings.Add(this);
-
 
             Init();
         }

+ 6 - 14
Jellyfin.Data/Entities/Series.cs

@@ -19,14 +19,6 @@ 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 Series CreateSeriesUnsafe()
-        {
-            return new Series();
-        }
-
         /// <summary>
         /// Public constructor with required data
         /// </summary>
@@ -57,27 +49,27 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Backing field for AirsDayOfWeek
         /// </summary>
-        protected Enums.Weekday? _AirsDayOfWeek;
+        protected DayOfWeek? _AirsDayOfWeek;
         /// <summary>
         /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting.
         /// </summary>
-        partial void SetAirsDayOfWeek(Enums.Weekday? oldValue, ref Enums.Weekday? newValue);
+        partial void SetAirsDayOfWeek(DayOfWeek? oldValue, ref DayOfWeek? newValue);
         /// <summary>
         /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning.
         /// </summary>
-        partial void GetAirsDayOfWeek(ref Enums.Weekday? result);
+        partial void GetAirsDayOfWeek(ref DayOfWeek? result);
 
-        public Enums.Weekday? AirsDayOfWeek
+        public DayOfWeek? AirsDayOfWeek
         {
             get
             {
-                Enums.Weekday? value = _AirsDayOfWeek;
+                DayOfWeek? value = _AirsDayOfWeek;
                 GetAirsDayOfWeek(ref value);
                 return (_AirsDayOfWeek = value);
             }
             set
             {
-                Enums.Weekday? oldValue = _AirsDayOfWeek;
+                DayOfWeek? oldValue = _AirsDayOfWeek;
                 SetAirsDayOfWeek(oldValue, ref value);
                 if (oldValue != value)
                 {

+ 386 - 114
Jellyfin.Data/Entities/User.cs

@@ -2,234 +2,506 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json.Serialization;
+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
     {
-        partial void Init();
+        /// <summary>
+        /// The values being delimited here are Guids, so commas work as they do not appear in Guids.
+        /// </summary>
+        private const char Delimiter = ',';
 
         /// <summary>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// Initializes a new instance of the <see cref="User"/> class.
+        /// Public constructor with required data.
         /// </summary>
-        protected User()
+        /// <param name="username">The username for the new user.</param>
+        /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+        /// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
+        public User(string username, string authenticationProviderId, string passwordResetProviderId)
         {
-            Groups = new HashSet<Group>();
+            if (string.IsNullOrEmpty(username))
+            {
+                throw new ArgumentNullException(nameof(username));
+            }
+
+            if (string.IsNullOrEmpty(authenticationProviderId))
+            {
+                throw new ArgumentNullException(nameof(authenticationProviderId));
+            }
+
+            if (string.IsNullOrEmpty(passwordResetProviderId))
+            {
+                throw new ArgumentNullException(nameof(passwordResetProviderId));
+            }
+
+            Username = username;
+            AuthenticationProviderId = authenticationProviderId;
+            PasswordResetProviderId = passwordResetProviderId;
+
+            AccessSchedules = new HashSet<AccessSchedule>();
+            // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
-            ProviderMappings = new HashSet<ProviderMapping>();
             Preferences = new HashSet<Preference>();
-
+            // ProviderMappings = new HashSet<ProviderMapping>();
+
+            // Set default values
+            Id = Guid.NewGuid();
+            InvalidLoginAttemptCount = 0;
+            EnableUserPreferenceAccess = true;
+            MustUpdatePassword = false;
+            DisplayMissingEpisodes = false;
+            DisplayCollectionsView = false;
+            HidePlayedInLatest = true;
+            RememberAudioSelections = true;
+            RememberSubtitleSelections = true;
+            EnableNextEpisodeAutoPlay = true;
+            EnableAutoLogin = false;
+            PlayDefaultAudioTrack = true;
+            SubtitleMode = SubtitlePlaybackMode.Default;
+            SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
+
+            AddDefaultPermissions();
+            AddDefaultPreferences();
             Init();
         }
 
         /// <summary>
-        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
-        /// </summary>
-        public static User CreateUserUnsafe()
-        {
-            return new User();
-        }
-
-        /// <summary>
-        /// Public constructor with required data
+        /// Initializes a new instance of the <see cref="User"/> class.
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
         /// </summary>
-        /// <param name="username"></param>
-        /// <param name="mustupdatepassword"></param>
-        /// <param name="audiolanguagepreference"></param>
-        /// <param name="authenticationproviderid"></param>
-        /// <param name="invalidloginattemptcount"></param>
-        /// <param name="subtitlemode"></param>
-        /// <param name="playdefaultaudiotrack"></param>
-        public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
+        protected User()
         {
-            if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
-            this.Username = username;
-
-            this.MustUpdatePassword = mustupdatepassword;
-
-            if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference));
-            this.AudioLanguagePreference = audiolanguagepreference;
-
-            if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid));
-            this.AuthenticationProviderId = authenticationproviderid;
-
-            this.InvalidLoginAttemptCount = invalidloginattemptcount;
-
-            if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode));
-            this.SubtitleMode = subtitlemode;
-
-            this.PlayDefaultAudioTrack = playdefaultaudiotrack;
-
-            this.Groups = new HashSet<Group>();
-            this.Permissions = new HashSet<Permission>();
-            this.ProviderMappings = new HashSet<ProviderMapping>();
-            this.Preferences = new HashSet<Preference>();
-
             Init();
         }
 
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="username"></param>
-        /// <param name="mustupdatepassword"></param>
-        /// <param name="audiolanguagepreference"></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 audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
-        {
-            return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack);
-        }
-
         /*************************************************************************
          * Properties
          *************************************************************************/
 
         /// <summary>
-        /// Identity, Indexed, Required
+        /// Gets or sets the Id of the user.
         /// </summary>
+        /// <remarks>
+        /// Identity, Indexed, Required.
+        /// </remarks>
         [Key]
         [Required]
-        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
-        public int Id { get; protected set; }
+        [JsonIgnore]
+        public Guid Id { get; set; }
 
         /// <summary>
-        /// Required, Max length = 255
+        /// Gets or sets the user's name.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 255.
+        /// </remarks>
         [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string Username { get; set; }
 
         /// <summary>
-        /// Max length = 65535
+        /// Gets or sets the user's password, or <c>null</c> if none is set.
         /// </summary>
+        /// <remarks>
+        /// Max length = 65535.
+        /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
         public string Password { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the user's easy password, or <c>null</c> if none is set.
+        /// </summary>
+        /// <remarks>
+        /// Max length = 65535.
+        /// </remarks>
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string EasyPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user must update their password.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool MustUpdatePassword { get; set; }
 
         /// <summary>
-        /// Required, Max length = 255
+        /// Gets or sets the audio language preference.
         /// </summary>
-        [Required]
+        /// <remarks>
+        /// Max length = 255.
+        /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
         public string AudioLanguagePreference { get; set; }
 
         /// <summary>
-        /// Required, Max length = 255
+        /// Gets or sets the authentication provider id.
         /// </summary>
+        /// <remarks>
+        /// Required, Max length = 255.
+        /// </remarks>
         [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string AuthenticationProviderId { get; set; }
 
         /// <summary>
-        /// Max length = 65535
+        /// Gets or sets the password reset provider id.
         /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string GroupedFolders { get; set; }
+        /// <remarks>
+        /// Required, Max length = 255.
+        /// </remarks>
+        [Required]
+        [MaxLength(255)]
+        [StringLength(255)]
+        public string PasswordResetProviderId { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets the invalid login attempt count.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public int InvalidLoginAttemptCount { get; set; }
 
         /// <summary>
-        /// Max length = 65535
+        /// Gets or sets the last activity date.
         /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string LatestItemExcludes { get; set; }
-
-        public int? LoginAttemptsBeforeLockout { get; set; }
+        public DateTime? LastActivityDate { get; set; }
 
         /// <summary>
-        /// Max length = 65535
+        /// Gets or sets the last login date.
         /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string MyMediaExcludes { get; set; }
+        public DateTime? LastLoginDate { get; set; }
 
         /// <summary>
-        /// Max length = 65535
+        /// Gets or sets the number of login attempts the user can make before they are locked out.
         /// </summary>
-        [MaxLength(65535)]
-        [StringLength(65535)]
-        public string OrderedViews { get; set; }
+        public int? LoginAttemptsBeforeLockout { get; set; }
 
         /// <summary>
-        /// Required, Max length = 255
+        /// Gets or sets the subtitle mode.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
-        [MaxLength(255)]
-        [StringLength(255)]
-        public string SubtitleMode { get; set; }
+        public SubtitlePlaybackMode SubtitleMode { get; set; }
 
         /// <summary>
-        /// Required
+        /// Gets or sets a value indicating whether the default audio track should be played.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool PlayDefaultAudioTrack { get; set; }
 
         /// <summary>
-        /// Max length = 255
+        /// Gets or sets the subtitle language preference.
         /// </summary>
+        /// <remarks>
+        /// Max length = 255.
+        /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string SubtitleLanguagePrefernce { get; set; }
+        public string SubtitleLanguagePreference { get; set; }
 
-        public bool? DisplayMissingEpisodes { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether missing episodes should be displayed.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool DisplayMissingEpisodes { get; set; }
 
-        public bool? DisplayCollectionsView { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether to display the collections view.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool DisplayCollectionsView { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user has a local password.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool EnableLocalPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the server should hide played content in "Latest".
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool HidePlayedInLatest { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to remember audio selections on played content.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool RememberAudioSelections { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to remember subtitle selections on played content.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool RememberSubtitleSelections { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to enable auto-play for the next episode.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool EnableNextEpisodeAutoPlay { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user should auto-login.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool EnableAutoLogin { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user can change their preferences.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public bool EnableUserPreferenceAccess { get; set; }
 
-        public bool? HidePlayedInLatest { get; set; }
+        /// <summary>
+        /// Gets or sets the maximum parental age rating.
+        /// </summary>
+        public int? MaxParentalAgeRating { get; set; }
 
-        public bool? RememberAudioSelections { get; set; }
+        /// <summary>
+        /// Gets or sets the remote client bitrate limit.
+        /// </summary>
+        public int? RemoteClientBitrateLimit { get; set; }
 
-        public bool? RememberSubtitleSelections { 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>
+        [Required]
+        public long InternalId { get; set; }
 
-        public bool? EnableNextEpisodeAutoPlay { get; set; }
+        /// <summary>
+        /// Gets or sets the user's profile image. Can be <c>null</c>.
+        /// </summary>
+        // [ForeignKey("UserId")]
+        public virtual ImageInfo ProfileImage { get; set; }
 
-        public bool? EnableUserPreferenceAccess { get; set; }
+        [Required]
+        public SyncPlayAccess SyncPlayAccess { get; set; }
 
         /// <summary>
-        /// Required, ConcurrenyToken
+        /// Gets or sets the row version.
         /// </summary>
+        /// <remarks>
+        /// Required, Concurrency Token.
+        /// </remarks>
         [ConcurrencyCheck]
         [Required]
         public uint RowVersion { get; set; }
 
-        public void OnSavingChanges()
-        {
-            RowVersion++;
-        }
-
         /*************************************************************************
          * Navigation properties
          *************************************************************************/
-        [ForeignKey("Group_Groups_Id")]
+
+        /// <summary>
+        /// Gets or sets the list of access schedules this user has.
+        /// </summary>
+        public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
+
+        /*
+        /// <summary>
+        /// Gets or sets the list of groups this user is a member of.
+        /// </summary>
+        [ForeignKey("Group_Groups_Guid")]
         public virtual ICollection<Group> Groups { get; protected set; }
+        */
 
-        [ForeignKey("Permission_Permissions_Id")]
+        /// <summary>
+        /// Gets or sets the list of permissions this user has.
+        /// </summary>
+        [ForeignKey("Permission_Permissions_Guid")]
         public virtual ICollection<Permission> Permissions { get; protected set; }
 
+        /*
+        /// <summary>
+        /// Gets or sets the list of provider mappings this user has.
+        /// </summary>
         [ForeignKey("ProviderMapping_ProviderMappings_Id")]
         public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+        */
 
-        [ForeignKey("Preference_Preferences_Id")]
+        /// <summary>
+        /// Gets or sets the list of preferences this user has.
+        /// </summary>
+        [ForeignKey("Preference_Preferences_Guid")]
         public virtual ICollection<Preference> Preferences { get; protected set; }
 
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="username">The username for the created user.</param>
+        /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+        /// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
+        /// <returns>The created instance.</returns>
+        public static User Create(string username, string authenticationProviderId, string passwordResetProviderId)
+        {
+            return new User(username, authenticationProviderId, passwordResetProviderId);
+        }
+
+        /// <inheritdoc/>
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /// <summary>
+        /// Checks whether the user has the specified permission.
+        /// </summary>
+        /// <param name="kind">The permission kind.</param>
+        /// <returns><c>True</c> if the user has the specified permission.</returns>
+        public bool HasPermission(PermissionKind kind)
+        {
+            return Permissions.First(p => p.Kind == kind).Value;
+        }
+
+        /// <summary>
+        /// Sets the given permission kind to the provided value.
+        /// </summary>
+        /// <param name="kind">The permission kind.</param>
+        /// <param name="value">The value to set.</param>
+        public void SetPermission(PermissionKind kind, bool value)
+        {
+            Permissions.First(p => p.Kind == kind).Value = value;
+        }
+
+        /// <summary>
+        /// Gets the user's preferences for the given preference kind.
+        /// </summary>
+        /// <param name="preference">The preference kind.</param>
+        /// <returns>A string array containing the user's preferences.</returns>
+        public string[] GetPreference(PreferenceKind preference)
+        {
+            var val = Preferences.First(p => p.Kind == preference).Value;
+
+            return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
+        }
+
+        /// <summary>
+        /// Sets the specified preference to the given value.
+        /// </summary>
+        /// <param name="preference">The preference kind.</param>
+        /// <param name="values">The values.</param>
+        public void SetPreference(PreferenceKind preference, string[] values)
+        {
+            Preferences.First(p => p.Kind == preference).Value
+                = string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
+        }
+
+        /// <summary>
+        /// Checks whether this user is currently allowed to use the server.
+        /// </summary>
+        /// <returns><c>True</c> if the current time is within an access schedule, or there are no access schedules.</returns>
+        public bool IsParentalScheduleAllowed()
+        {
+            return AccessSchedules.Count == 0
+                   || AccessSchedules.Any(i => IsParentalScheduleAllowed(i, DateTime.UtcNow));
+        }
+
+        /// <summary>
+        /// Checks whether the provided folder is in this user's grouped folders.
+        /// </summary>
+        /// <param name="id">The Guid of the folder.</param>
+        /// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
+        public bool IsFolderGrouped(Guid id)
+        {
+            return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
+        }
+
+        private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
+        {
+            var localTime = date.ToLocalTime();
+            var hour = localTime.TimeOfDay.TotalHours;
+
+            return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+                   && 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, true));
+            Permissions.Add(new Permission(PermissionKind.EnableAllChannels, true));
+            Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true));
+            Permissions.Add(new Permission(PermissionKind.EnableAllFolders, true));
+            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));
+            }
+        }
+
+        partial void Init();
     }
 }
-

+ 1 - 1
MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs → Jellyfin.Data/Enums/DynamicDayOfWeek.cs

@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
 {
     public enum DynamicDayOfWeek
     {

+ 107 - 20
Jellyfin.Data/Enums/PermissionKind.cs

@@ -1,26 +1,113 @@
 namespace Jellyfin.Data.Enums
 {
+    /// <summary>
+    /// The types of user permissions.
+    /// </summary>
     public enum PermissionKind
     {
-        IsAdministrator,
-        IsHidden,
-        IsDisabled,
-        BlockUnrateditems,
-        EnbleSharedDeviceControl,
-        EnableRemoteAccess,
-        EnableLiveTvManagement,
-        EnableLiveTvAccess,
-        EnableMediaPlayback,
-        EnableAudioPlaybackTranscoding,
-        EnableVideoPlaybackTranscoding,
-        EnableContentDeletion,
-        EnableContentDownloading,
-        EnableSyncTranscoding,
-        EnableMediaConversion,
-        EnableAllDevices,
-        EnableAllChannels,
-        EnableAllFolders,
-        EnablePublicSharing,
-        AccessSchedules
+        /// <summary>
+        /// Whether the user is an administrator.
+        /// </summary>
+        IsAdministrator = 0,
+
+        /// <summary>
+        /// Whether the user is hidden.
+        /// </summary>
+        IsHidden = 1,
+
+        /// <summary>
+        /// Whether the user is disabled.
+        /// </summary>
+        IsDisabled = 2,
+
+        /// <summary>
+        /// Whether the user can control shared devices.
+        /// </summary>
+        EnableSharedDeviceControl = 3,
+
+        /// <summary>
+        /// Whether the user can access the server remotely.
+        /// </summary>
+        EnableRemoteAccess = 4,
+
+        /// <summary>
+        /// Whether the user can manage live tv.
+        /// </summary>
+        EnableLiveTvManagement = 5,
+
+        /// <summary>
+        /// Whether the user can access live tv.
+        /// </summary>
+        EnableLiveTvAccess = 6,
+
+        /// <summary>
+        /// Whether the user can play media.
+        /// </summary>
+        EnableMediaPlayback = 7,
+
+        /// <summary>
+        /// Whether the server should transcode audio for the user if requested.
+        /// </summary>
+        EnableAudioPlaybackTranscoding = 8,
+
+        /// <summary>
+        /// Whether the server should transcode video for the user if requested.
+        /// </summary>
+        EnableVideoPlaybackTranscoding = 9,
+
+        /// <summary>
+        /// Whether the user can delete content.
+        /// </summary>
+        EnableContentDeletion = 10,
+
+        /// <summary>
+        /// Whether the user can download content.
+        /// </summary>
+        EnableContentDownloading = 11,
+
+        /// <summary>
+        /// Whether to enable sync transcoding for the user.
+        /// </summary>
+        EnableSyncTranscoding = 12,
+
+        /// <summary>
+        /// Whether the user can do media conversion.
+        /// </summary>
+        EnableMediaConversion = 13,
+
+        /// <summary>
+        /// Whether the user has access to all devices.
+        /// </summary>
+        EnableAllDevices = 14,
+
+        /// <summary>
+        /// Whether the user has access to all channels.
+        /// </summary>
+        EnableAllChannels = 15,
+
+        /// <summary>
+        /// Whether the user has access to all folders.
+        /// </summary>
+        EnableAllFolders = 16,
+
+        /// <summary>
+        /// Whether to enable public sharing for the user.
+        /// </summary>
+        EnablePublicSharing = 17,
+
+        /// <summary>
+        /// Whether the user can remotely control other users.
+        /// </summary>
+        EnableRemoteControlOfOtherUsers = 18,
+
+        /// <summary>
+        /// Whether the user is permitted to do playback remuxing.
+        /// </summary>
+        EnablePlaybackRemuxing = 19,
+
+        /// <summary>
+        /// Whether the server should force transcoding on remote connections for the user.
+        /// </summary>
+        ForceRemoteSourceTranscoding = 20
     }
 }

+ 62 - 7
Jellyfin.Data/Enums/PreferenceKind.cs

@@ -1,13 +1,68 @@
 namespace Jellyfin.Data.Enums
 {
+    /// <summary>
+    /// The types of user preferences.
+    /// </summary>
     public enum PreferenceKind
     {
-        MaxParentalRating,
-        BlockedTags,
-        RemoteClientBitrateLimit,
-        EnabledDevices,
-        EnabledChannels,
-        EnabledFolders,
-        EnableContentDeletionFromFolders
+        /// <summary>
+        /// A list of blocked tags.
+        /// </summary>
+        BlockedTags = 0,
+
+        /// <summary>
+        /// A list of blocked channels.
+        /// </summary>
+        BlockedChannels = 1,
+
+        /// <summary>
+        /// A list of blocked media folders.
+        /// </summary>
+        BlockedMediaFolders = 2,
+
+        /// <summary>
+        /// A list of enabled devices.
+        /// </summary>
+        EnabledDevices = 3,
+
+        /// <summary>
+        /// A list of enabled channels
+        /// </summary>
+        EnabledChannels = 4,
+
+        /// <summary>
+        /// A list of enabled folders.
+        /// </summary>
+        EnabledFolders = 5,
+
+        /// <summary>
+        /// A list of folders to allow content deletion from.
+        /// </summary>
+        EnableContentDeletionFromFolders = 6,
+
+        /// <summary>
+        /// A list of latest items to exclude.
+        /// </summary>
+        LatestItemExcludes = 7,
+
+        /// <summary>
+        /// A list of media to exclude.
+        /// </summary>
+        MyMediaExcludes = 8,
+
+        /// <summary>
+        /// A list of grouped folders.
+        /// </summary>
+        GroupedFolders = 9,
+
+        /// <summary>
+        /// A list of unrated items to block.
+        /// </summary>
+        BlockUnratedItems = 10,
+
+        /// <summary>
+        /// A list of ordered views.
+        /// </summary>
+        OrderedViews = 11
     }
 }

+ 2 - 2
MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs → Jellyfin.Data/Enums/SubtitlePlaybackMode.cs

@@ -1,6 +1,6 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
 {
     public enum SubtitlePlaybackMode
     {

+ 4 - 4
MediaBrowser.Model/Configuration/SyncplayAccess.cs → Jellyfin.Data/Enums/SyncPlayAccess.cs

@@ -1,4 +1,4 @@
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
 {
     /// <summary>
     /// Enum SyncPlayAccess.
@@ -8,16 +8,16 @@ namespace MediaBrowser.Model.Configuration
         /// <summary>
         /// User can create groups and join them.
         /// </summary>
-        CreateAndJoinGroups,
+        CreateAndJoinGroups = 0,
 
         /// <summary>
         /// User can only join already existing groups.
         /// </summary>
-        JoinGroups,
+        JoinGroups = 1,
 
         /// <summary>
         /// SyncPlay is disabled for the user.
         /// </summary>
-        None
+        None = 2
     }
 }

+ 1 - 1
MediaBrowser.Model/Configuration/UnratedItem.cs → Jellyfin.Data/Enums/UnratedItem.cs

@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 
-namespace MediaBrowser.Model.Configuration
+namespace Jellyfin.Data.Enums
 {
     public enum UnratedItem
     {

+ 0 - 13
Jellyfin.Data/Enums/Weekday.cs

@@ -1,13 +0,0 @@
-namespace Jellyfin.Data.Enums
-{
-    public enum Weekday
-    {
-        Sunday,
-        Monday,
-        Tuesday,
-        Wednesday,
-        Thursday,
-        Friday,
-        Saturday
-    }
-}

+ 31 - 0
Jellyfin.Data/IHasPermissions.cs

@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data
+{
+    /// <summary>
+    /// An abstraction representing an entity that has permissions.
+    /// </summary>
+    public interface IHasPermissions
+    {
+        /// <summary>
+        /// Gets a collection containing this entity's permissions.
+        /// </summary>
+        ICollection<Permission> Permissions { get; }
+
+        /// <summary>
+        /// Checks whether this entity has the specified permission kind.
+        /// </summary>
+        /// <param name="kind">The kind of permission.</param>
+        /// <returns><c>true</c> if this entity has the specified permission, <c>false</c> otherwise.</returns>
+        bool HasPermission(PermissionKind kind);
+
+        /// <summary>
+        /// Sets the specified permission to the provided value.
+        /// </summary>
+        /// <param name="kind">The kind of permission.</param>
+        /// <param name="value">The value to set.</param>
+        void SetPermission(PermissionKind kind, bool value);
+    }
+}

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

@@ -21,6 +21,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" />
   </ItemGroup>
 
 </Project>

+ 0 - 2
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -21,8 +21,6 @@
 
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs" />
-    <Compile Remove="Migrations\20200430214405_InitialSchema.cs" />
-    <Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
   </ItemGroup>
 
   <ItemGroup>

+ 39 - 31
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -1,9 +1,4 @@
 #pragma warning disable CS1591
-#pragma warning disable SA1201 // Constuctors should not follow properties
-#pragma warning disable SA1516 // Elements should be followed by a blank line
-#pragma warning disable SA1623 // Property's documentation should begin with gets or sets
-#pragma warning disable SA1629 // Documentation should end with a period
-#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class
 
 using System.Linq;
 using Jellyfin.Data;
@@ -15,7 +10,30 @@ namespace Jellyfin.Server.Implementations
     /// <inheritdoc/>
     public partial class JellyfinDb : DbContext
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="JellyfinDb"/> class.
+        /// </summary>
+        /// <param name="options">The database context options.</param>
+        public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the default connection string.
+        /// </summary>
+        public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
+
+        public virtual DbSet<AccessSchedule> AccessSchedules { get; set; }
+
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+
+        public virtual DbSet<ImageInfo> ImageInfos { get; set; }
+
+        public virtual DbSet<Permission> Permissions { get; set; }
+
+        public virtual DbSet<Preference> Preferences { get; set; }
+
+        public virtual DbSet<User> Users { get; set; }
         /*public virtual DbSet<Artwork> Artwork { get; set; }
         public virtual DbSet<Book> Books { get; set; }
         public virtual DbSet<BookMetadata> BookMetadata { get; set; }
@@ -42,12 +60,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; }
 
@@ -62,20 +78,21 @@ namespace Jellyfin.Server.Implementations
         public virtual DbSet<Series> Series { get; set; }
         public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
         public virtual DbSet<Track> Tracks { get; set; }
-        public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }
-        public virtual DbSet<User> Users { get; set; } */
-
-        /// <summary>
-        /// Gets or sets the default connection string.
-        /// </summary>
-        public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
+        public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }*/
 
-        /// <inheritdoc />
-        public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
+        /// <inheritdoc/>
+        public override int SaveChanges()
         {
-        }
+            foreach (var saveEntity in ChangeTracker.Entries()
+                .Where(e => e.State == EntityState.Modified)
+                .Select(entry => entry.Entity)
+                .OfType<ISavingChanges>())
+            {
+                saveEntity.OnSavingChanges();
+            }
 
-        partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
+            return base.SaveChanges();
+        }
 
         /// <inheritdoc />
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
@@ -83,9 +100,6 @@ namespace Jellyfin.Server.Implementations
             CustomInit(optionsBuilder);
         }
 
-        partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
-        partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
-
         /// <inheritdoc />
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
@@ -105,16 +119,10 @@ namespace Jellyfin.Server.Implementations
             OnModelCreatedImpl(modelBuilder);
         }
 
-        public override int SaveChanges()
-        {
-            foreach (var saveEntity in ChangeTracker.Entries()
-                .Where(e => e.State == EntityState.Modified)
-                .OfType<ISavingChanges>())
-            {
-                saveEntity.OnSavingChanges();
-            }
+        partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
 
-            return base.SaveChanges();
-        }
+        partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
+
+        partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
     }
 }

+ 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>

+ 312 - 0
Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs

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

+ 197 - 0
Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs

@@ -0,0 +1,197 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class AddUsers : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            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: true),
+                    AuthenticationProviderId = table.Column<string>(maxLength: 255, nullable: false),
+                    PasswordResetProviderId = table.Column<string>(maxLength: 255, nullable: false),
+                    InvalidLoginAttemptCount = table.Column<int>(nullable: false),
+                    LastActivityDate = table.Column<DateTime>(nullable: true),
+                    LastLoginDate = table.Column<DateTime>(nullable: true),
+                    LoginAttemptsBeforeLockout = table.Column<int>(nullable: true),
+                    SubtitleMode = table.Column<int>(nullable: false),
+                    PlayDefaultAudioTrack = table.Column<bool>(nullable: false),
+                    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),
+                    SyncPlayAccess = table.Column<int>(nullable: false),
+                    RowVersion = table.Column<uint>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Users", x => x.Id);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "AccessSchedules",
+                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_AccessSchedules", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_AccessSchedules_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "ImageInfos",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: true),
+                    Path = table.Column<string>(maxLength: 512, nullable: false),
+                    LastModified = table.Column<DateTime>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ImageInfos", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_ImageInfos_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Restrict);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Permissions",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    Kind = table.Column<int>(nullable: false),
+                    Value = table.Column<bool>(nullable: false),
+                    RowVersion = table.Column<uint>(nullable: false),
+                    Permission_Permissions_Guid = table.Column<Guid>(nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Permissions", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_Permissions_Users_Permission_Permissions_Guid",
+                        column: x => x.Permission_Permissions_Guid,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Restrict);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Preferences",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    Kind = table.Column<int>(nullable: false),
+                    Value = table.Column<string>(maxLength: 65535, nullable: false),
+                    RowVersion = table.Column<uint>(nullable: false),
+                    Preference_Preferences_Guid = table.Column<Guid>(nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Preferences", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_Preferences_Users_Preference_Preferences_Guid",
+                        column: x => x.Preference_Preferences_Guid,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Restrict);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_AccessSchedules_UserId",
+                schema: "jellyfin",
+                table: "AccessSchedules",
+                column: "UserId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ImageInfos_UserId",
+                schema: "jellyfin",
+                table: "ImageInfos",
+                column: "UserId",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                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: "Preferences",
+                column: "Preference_Preferences_Guid");
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "AccessSchedules",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "ImageInfos",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "Permissions",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "Preferences",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "Users",
+                schema: "jellyfin");
+        }
+    }
+}

+ 243 - 1
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
 {
@@ -13,7 +15,32 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.3");
+                .HasAnnotation("ProductVersion", "3.1.4");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
                 {
@@ -60,6 +87,221 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.ToTable("ActivityLogs");
                 });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Permission_Permissions_Guid");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Preference_Preferences_Guid");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("Permission_Permissions_Guid");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("Preference_Preferences_Guid");
+                });
 #pragma warning restore 612, 618
         }
     }

+ 9 - 7
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs → Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs

@@ -1,14 +1,16 @@
+#nullable enable
+
 using System;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Cryptography;
 
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// The default authentication provider.
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library
         {
             if (resolvedUser == null)
             {
-                throw new AuthenticationException($"Specified user does not exist.");
+                throw new AuthenticationException("Specified user does not exist.");
             }
 
             bool success = false;
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.Library
                 });
             }
 
-            byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
+            byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
 
             PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
             if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
@@ -69,7 +71,7 @@ namespace Emby.Server.Implementations.Library
             {
                 byte[] calculatedHash = _cryptographyProvider.ComputeHash(
                     readyHash.Id,
-                    passwordbytes,
+                    passwordBytes,
                     readyHash.Salt.ToArray());
 
                 if (readyHash.Hash.SequenceEqual(calculatedHash))
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Library
 
         /// <inheritdoc />
         public bool HasPassword(User user)
-            => !string.IsNullOrEmpty(user.Password);
+            => !string.IsNullOrEmpty(user?.Password);
 
         /// <inheritdoc />
         public Task ChangePassword(User user, string newPassword)
@@ -129,7 +131,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <inheritdoc />
-        public string GetEasyPasswordHash(User user)
+        public string? GetEasyPasswordHash(User user)
         {
             return string.IsNullOrEmpty(user.EasyPassword)
                 ? null

+ 30 - 29
Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs → Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -1,8 +1,11 @@
+#nullable enable
+
 using System;
 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 +13,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// The default password reset provider.
@@ -51,54 +54,49 @@ namespace Emby.Server.Implementations.Library
         /// <inheritdoc />
         public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
         {
-            SerializablePasswordReset spr;
-            List<string> usersreset = new List<string>();
-            foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
+            var usersReset = new List<string>();
+            foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
             {
-                using (var str = File.OpenRead(resetfile))
+                SerializablePasswordReset spr;
+                await using (var str = File.OpenRead(resetFile))
                 {
                     spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
                 }
 
-                if (spr.ExpirationDate < DateTime.Now)
+                if (spr.ExpirationDate < DateTime.UtcNow)
                 {
-                    File.Delete(resetfile);
+                    File.Delete(resetFile);
                 }
                 else if (string.Equals(
                     spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     StringComparison.InvariantCultureIgnoreCase))
                 {
-                    var resetUser = _userManager.GetUserByName(spr.UserName);
-                    if (resetUser == null)
-                    {
-                        throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
-                    }
+                    var resetUser = _userManager.GetUserByName(spr.UserName)
+                        ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
 
                     await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
-                    usersreset.Add(resetUser.Name);
-                    File.Delete(resetfile);
+                    usersReset.Add(resetUser.Username);
+                    File.Delete(resetFile);
                 }
             }
 
-            if (usersreset.Count < 1)
+            if (usersReset.Count < 1)
             {
                 throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
             }
-            else
+
+            return new PinRedeemResult
             {
-                return new PinRedeemResult
-                {
-                    Success = true,
-                    UsersReset = usersreset.ToArray()
-                };
-            }
+                Success = true,
+                UsersReset = usersReset.ToArray()
+            };
         }
 
         /// <inheritdoc />
-        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
+        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
         {
-            string pin = string.Empty;
+            string pin;
             using (var cryptoRandom = RandomNumberGenerator.Create())
             {
                 byte[] bytes = new byte[4];
@@ -106,30 +104,33 @@ namespace Emby.Server.Implementations.Library
                 pin = BitConverter.ToString(bytes);
             }
 
-            DateTime expireTime = DateTime.Now.AddMinutes(30);
-            string filePath = _passwordResetFileBase + user.InternalId + ".json";
+            DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
+            string filePath = _passwordResetFileBase + user.Id + ".json";
             SerializablePasswordReset spr = new SerializablePasswordReset
             {
                 ExpirationDate = expireTime,
                 Pin = pin,
                 PinFile = filePath,
-                UserName = user.Name
+                UserName = user.Username
             };
 
-            using (FileStream fileStream = File.OpenWrite(filePath))
+            await using (FileStream fileStream = File.OpenWrite(filePath))
             {
                 _jsonSerializer.SerializeToStream(spr, fileStream);
                 await fileStream.FlushAsync().ConfigureAwait(false);
             }
 
+            user.EasyPassword = pin;
+            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
             return new ForgotPasswordResult
             {
                 Action = ForgotPasswordAction.PinCode,
                 PinExpirationDate = expireTime,
-                PinFile = filePath
             };
         }
 
+#nullable disable
         private class SerializablePasswordReset : PasswordPinCreationResult
         {
             public string Pin { get; set; }

+ 67 - 0
Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs

@@ -0,0 +1,67 @@
+#nullable enable
+#pragma warning disable CS1591
+
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Events;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+    public sealed class DeviceAccessEntryPoint : IServerEntryPoint
+    {
+        private readonly IUserManager _userManager;
+        private readonly IAuthenticationRepository _authRepo;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ISessionManager _sessionManager;
+
+        public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+        {
+            _userManager = userManager;
+            _authRepo = authRepo;
+            _deviceManager = deviceManager;
+            _sessionManager = sessionManager;
+        }
+
+        public Task RunAsync()
+        {
+            _userManager.OnUserUpdated += OnUserUpdated;
+
+            return Task.CompletedTask;
+        }
+
+        public void Dispose()
+        {
+        }
+
+        private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+        {
+            var user = e.Argument;
+            if (!user.HasPermission(PermissionKind.EnableAllDevices))
+            {
+                UpdateDeviceAccess(user);
+            }
+        }
+
+        private void UpdateDeviceAccess(User user)
+        {
+            var existing = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                UserId = user.Id
+            }).Items;
+
+            foreach (var authInfo in existing)
+            {
+                if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+                {
+                    _sessionManager.Logout(authInfo);
+                }
+            }
+        }
+    }
+}

+ 4 - 8
Emby.Server.Implementations/Library/InvalidAuthProvider.cs → Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs

@@ -1,8 +1,10 @@
+#nullable enable
+
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
 
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// An invalid authentication provider.
@@ -39,12 +41,6 @@ namespace Emby.Server.Implementations.Library
             // Nothing here
         }
 
-        /// <inheritdoc />
-        public string GetPasswordHash(User user)
-        {
-            return string.Empty;
-        }
-
         /// <inheritdoc />
         public string GetEasyPasswordHash(User user)
         {

+ 841 - 0
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -0,0 +1,841 @@
+#nullable enable
+#pragma warning disable CA1307
+
+using System;
+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;
+using MediaBrowser.Common.Cryptography;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Users
+{
+    /// <summary>
+    /// Manages the creation and retrieval of <see cref="User"/> instances.
+    /// </summary>
+    public class UserManager : IUserManager
+    {
+        private readonly JellyfinDbProvider _dbProvider;
+        private readonly ICryptoProvider _cryptoProvider;
+        private readonly INetworkManager _networkManager;
+        private readonly IApplicationHost _appHost;
+        private readonly IImageProcessor _imageProcessor;
+        private readonly ILogger<UserManager> _logger;
+
+        private IAuthenticationProvider[] _authenticationProviders = null!;
+        private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
+        private InvalidAuthProvider _invalidAuthProvider = null!;
+        private IPasswordResetProvider[] _passwordResetProviders = null!;
+        private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserManager"/> class.
+        /// </summary>
+        /// <param name="dbProvider">The database provider.</param>
+        /// <param name="cryptoProvider">The cryptography provider.</param>
+        /// <param name="networkManager">The network manager.</param>
+        /// <param name="appHost">The application host.</param>
+        /// <param name="imageProcessor">The image processor.</param>
+        /// <param name="logger">The logger.</param>
+        public UserManager(
+            JellyfinDbProvider dbProvider,
+            ICryptoProvider cryptoProvider,
+            INetworkManager networkManager,
+            IApplicationHost appHost,
+            IImageProcessor imageProcessor,
+            ILogger<UserManager> logger)
+        {
+            _dbProvider = dbProvider;
+            _cryptoProvider = cryptoProvider;
+            _networkManager = networkManager;
+            _appHost = appHost;
+            _imageProcessor = imageProcessor;
+            _logger = logger;
+        }
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<User>>? OnUserPasswordChanged;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<User>>? OnUserCreated;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<User>>? OnUserDeleted;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
+
+        /// <inheritdoc/>
+        public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
+
+        /// <inheritdoc/>
+        public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
+
+        /// <inheritdoc/>
+        public User? GetUserById(Guid id)
+        {
+            if (id == Guid.Empty)
+            {
+                throw new ArgumentException("Guid can't be empty", nameof(id));
+            }
+
+            return _dbProvider.CreateContext().Users.Find(id);
+        }
+
+        /// <inheritdoc/>
+        public User? GetUserByName(string name)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentException("Invalid username", nameof(name));
+            }
+
+            // This can't use an overload with StringComparer because that would cause the query to
+            // have to be evaluated client-side.
+            return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name));
+        }
+
+        /// <inheritdoc/>
+        public async Task RenameUser(User user, string newName)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            if (string.IsNullOrWhiteSpace(newName))
+            {
+                throw new ArgumentException("Invalid username", nameof(newName));
+            }
+
+            if (user.Username.Equals(newName, StringComparison.Ordinal))
+            {
+                throw new ArgumentException("The new and old names must be different.");
+            }
+
+            if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+            {
+                throw new ArgumentException(string.Format(
+                    CultureInfo.InvariantCulture,
+                    "A user with the name '{0}' already exists.",
+                    newName));
+            }
+
+            user.Username = newName;
+            await UpdateUserAsync(user).ConfigureAwait(false);
+
+            OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        /// <inheritdoc/>
+        public void UpdateUser(User user)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            dbContext.Users.Update(user);
+            dbContext.SaveChanges();
+        }
+
+        /// <inheritdoc/>
+        public async Task UpdateUserAsync(User user)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            dbContext.Users.Update(user);
+
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+        }
+
+        /// <inheritdoc/>
+        public User CreateUser(string name)
+        {
+            if (!IsValidUsername(name))
+            {
+                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+            }
+
+            var dbContext = _dbProvider.CreateContext();
+
+            // TODO: Remove after user item data is migrated.
+            var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
+
+            var newUser = new User(
+                name,
+                _defaultAuthenticationProvider.GetType().FullName,
+                _defaultPasswordResetProvider.GetType().FullName)
+            {
+                InternalId = max + 1
+            };
+            dbContext.Users.Add(newUser);
+            dbContext.SaveChanges();
+
+            OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
+
+            return newUser;
+        }
+
+        /// <inheritdoc/>
+        public void DeleteUser(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            var dbContext = _dbProvider.CreateContext();
+
+            if (dbContext.Users.Find(user.Id) == null)
+            {
+                throw new ArgumentException(string.Format(
+                    CultureInfo.InvariantCulture,
+                    "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
+                    user.Username,
+                    user.Id));
+            }
+
+            if (dbContext.Users.Count() == 1)
+            {
+                throw new InvalidOperationException(string.Format(
+                    CultureInfo.InvariantCulture,
+                    "The user '{0}' cannot be deleted because there must be at least one user in the system.",
+                    user.Username));
+            }
+
+            if (user.HasPermission(PermissionKind.IsAdministrator)
+                && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
+            {
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
+                        user.Username),
+                    nameof(user));
+            }
+
+            dbContext.Users.Remove(user);
+            dbContext.SaveChanges();
+            OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        /// <inheritdoc/>
+        public Task ResetPassword(User user)
+        {
+            return ChangePassword(user, string.Empty);
+        }
+
+        /// <inheritdoc/>
+        public void ResetEasyPassword(User user)
+        {
+            ChangeEasyPassword(user, string.Empty, null);
+        }
+
+        /// <inheritdoc/>
+        public async Task ChangePassword(User user, string newPassword)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
+            await UpdateUserAsync(user).ConfigureAwait(false);
+
+            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        /// <inheritdoc/>
+        public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
+        {
+            GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
+            UpdateUser(user);
+
+            OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        /// <inheritdoc/>
+        public UserDto GetUserDto(User user, string? remoteEndPoint = null)
+        {
+            var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+            return new UserDto
+            {
+                Name = user.Username,
+                Id = user.Id,
+                ServerId = _appHost.SystemId,
+                HasPassword = hasPassword,
+                HasConfiguredPassword = hasPassword,
+                HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword),
+                EnableAutoLogin = user.EnableAutoLogin,
+                LastLoginDate = user.LastLoginDate,
+                LastActivityDate = user.LastActivityDate,
+                PrimaryImageTag = user.ProfileImage != null ? _imageProcessor.GetImageCacheTag(user) : null,
+                Configuration = new UserConfiguration
+                {
+                    SubtitleMode = user.SubtitleMode,
+                    HidePlayedInLatest = user.HidePlayedInLatest,
+                    EnableLocalPassword = user.EnableLocalPassword,
+                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
+                    DisplayCollectionsView = user.DisplayCollectionsView,
+                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
+                    AudioLanguagePreference = user.AudioLanguagePreference,
+                    RememberAudioSelections = user.RememberAudioSelections,
+                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
+                    RememberSubtitleSelections = user.RememberSubtitleSelections,
+                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
+                    OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
+                    GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
+                    MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
+                    LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
+                },
+                Policy = new UserPolicy
+                {
+                    MaxParentalRating = user.MaxParentalAgeRating,
+                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
+                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
+                    AuthenticationProviderId = user.AuthenticationProviderId,
+                    PasswordResetProviderId = user.PasswordResetProviderId,
+                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
+                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
+                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
+                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
+                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
+                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
+                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
+                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
+                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
+                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
+                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
+                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
+                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
+                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
+                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
+                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
+                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
+                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
+                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers),
+                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
+                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
+                    AccessSchedules = user.AccessSchedules.ToArray(),
+                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
+                    EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels),
+                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
+                    EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders),
+                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
+                    SyncPlayAccess = user.SyncPlayAccess,
+                    BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels),
+                    BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders),
+                    BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
+                }
+            };
+        }
+
+        /// <inheritdoc/>
+        public async Task<User?> AuthenticateUser(
+            string username,
+            string password,
+            string passwordSha1,
+            string remoteEndPoint,
+            bool isUserSession)
+        {
+            if (string.IsNullOrWhiteSpace(username))
+            {
+                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
+                throw new ArgumentNullException(nameof(username));
+            }
+
+            var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+            bool success;
+            IAuthenticationProvider? authenticationProvider;
+
+            if (user != null)
+            {
+                var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+                    .ConfigureAwait(false);
+                authenticationProvider = authResult.authenticationProvider;
+                success = authResult.success;
+            }
+            else
+            {
+                var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint)
+                    .ConfigureAwait(false);
+                authenticationProvider = authResult.authenticationProvider;
+                string updatedUsername = authResult.username;
+                success = authResult.success;
+
+                if (success
+                    && authenticationProvider != null
+                    && !(authenticationProvider is DefaultAuthenticationProvider))
+                {
+                    // Trust the username returned by the authentication provider
+                    username = updatedUsername;
+
+                    // Search the database for the user again
+                    // the authentication provider might have created it
+                    user = Users
+                        .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+
+                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
+                    {
+                        UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
+
+                        await UpdateUserAsync(user).ConfigureAwait(false);
+                    }
+                }
+            }
+
+            if (success && user != null && authenticationProvider != null)
+            {
+                var providerId = authenticationProvider.GetType().FullName;
+
+                if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+                {
+                    user.AuthenticationProviderId = providerId;
+                    await UpdateUserAsync(user).ConfigureAwait(false);
+                }
+            }
+
+            if (user == null)
+            {
+                _logger.LogInformation(
+                    "Authentication request for {UserName} has been denied (IP: {IP}).",
+                    username,
+                    remoteEndPoint);
+                throw new AuthenticationException("Invalid username or password entered.");
+            }
+
+            if (user.HasPermission(PermissionKind.IsDisabled))
+            {
+                _logger.LogInformation(
+                    "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
+                    username,
+                    remoteEndPoint);
+                throw new SecurityException(
+                    $"The {user.Username} account is currently disabled. Please consult with your administrator.");
+            }
+
+            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
+                !_networkManager.IsInLocalNetwork(remoteEndPoint))
+            {
+                _logger.LogInformation(
+                    "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
+                    username,
+                    remoteEndPoint);
+                throw new SecurityException("Forbidden.");
+            }
+
+            if (!user.IsParentalScheduleAllowed())
+            {
+                _logger.LogInformation(
+                    "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
+                    username,
+                    remoteEndPoint);
+                throw new SecurityException("User is not allowed access at this time.");
+            }
+
+            // Update LastActivityDate and LastLoginDate, then save
+            if (success)
+            {
+                if (isUserSession)
+                {
+                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
+                }
+
+                user.InvalidLoginAttemptCount = 0;
+                await UpdateUserAsync(user).ConfigureAwait(false);
+                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
+            }
+            else
+            {
+                IncrementInvalidLoginAttemptCount(user);
+                _logger.LogInformation(
+                    "Authentication request for {UserName} has been denied (IP: {IP}).",
+                    user.Username,
+                    remoteEndPoint);
+            }
+
+            return success ? user : null;
+        }
+
+        /// <inheritdoc/>
+        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
+        {
+            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
+
+            if (user != null && isInNetwork)
+            {
+                var passwordResetProvider = GetPasswordResetProvider(user);
+                return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
+            }
+
+            return new ForgotPasswordResult
+            {
+                Action = ForgotPasswordAction.InNetworkRequired,
+                PinFile = string.Empty
+            };
+        }
+
+        /// <inheritdoc/>
+        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
+        {
+            foreach (var provider in _passwordResetProviders)
+            {
+                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
+
+                if (result.Success)
+                {
+                    return result;
+                }
+            }
+
+            return new PinRedeemResult
+            {
+                Success = false,
+                UsersReset = Array.Empty<string>()
+            };
+        }
+
+        /// <inheritdoc/>
+        public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
+        {
+            _authenticationProviders = authenticationProviders.ToArray();
+            _passwordResetProviders = passwordResetProviders.ToArray();
+
+            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
+            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
+        }
+
+        /// <inheritdoc />
+        public void Initialize()
+        {
+            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
+            var dbContext = _dbProvider.CreateContext();
+
+            if (dbContext.Users.Any())
+            {
+                return;
+            }
+
+            var defaultName = Environment.UserName;
+            if (string.IsNullOrWhiteSpace(defaultName))
+            {
+                defaultName = "MyJellyfinUser";
+            }
+
+            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
+
+            if (!IsValidUsername(defaultName))
+            {
+                throw new ArgumentException("Provided username is not valid!", defaultName);
+            }
+
+            var newUser = CreateUser(defaultName);
+            newUser.SetPermission(PermissionKind.IsAdministrator, true);
+            newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
+            newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
+
+            dbContext.Users.Update(newUser);
+            dbContext.SaveChanges();
+        }
+
+        /// <inheritdoc/>
+        public NameIdPair[] GetAuthenticationProviders()
+        {
+            return _authenticationProviders
+                .Where(provider => provider.IsEnabled)
+                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
+                .ThenBy(i => i.Name)
+                .Select(i => new NameIdPair
+                {
+                    Name = i.Name,
+                    Id = i.GetType().FullName
+                })
+                .ToArray();
+        }
+
+        /// <inheritdoc/>
+        public NameIdPair[] GetPasswordResetProviders()
+        {
+            return _passwordResetProviders
+                .Where(provider => provider.IsEnabled)
+                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
+                .ThenBy(i => i.Name)
+                .Select(i => new NameIdPair
+                {
+                    Name = i.Name,
+                    Id = i.GetType().FullName
+                })
+                .ToArray();
+        }
+
+        /// <inheritdoc/>
+        public void UpdateConfiguration(Guid userId, UserConfiguration config)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+            user.SubtitleMode = config.SubtitleMode;
+            user.HidePlayedInLatest = config.HidePlayedInLatest;
+            user.EnableLocalPassword = config.EnableLocalPassword;
+            user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
+            user.DisplayCollectionsView = config.DisplayCollectionsView;
+            user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
+            user.AudioLanguagePreference = config.AudioLanguagePreference;
+            user.RememberAudioSelections = config.RememberAudioSelections;
+            user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
+            user.RememberSubtitleSelections = config.RememberSubtitleSelections;
+            user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+
+            user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+            user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+            user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+            user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+            dbContext.Update(user);
+            dbContext.SaveChanges();
+        }
+
+        /// <inheritdoc/>
+        public void UpdatePolicy(Guid userId, UserPolicy policy)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+
+            // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
+            int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+            {
+                -1 => null,
+                0 => 3,
+                _ => policy.LoginAttemptsBeforeLockout
+            };
+
+            user.MaxParentalAgeRating = policy.MaxParentalRating;
+            user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
+            user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
+            user.AuthenticationProviderId = policy.AuthenticationProviderId;
+            user.PasswordResetProviderId = policy.PasswordResetProviderId;
+            user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
+            user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+            user.SyncPlayAccess = policy.SyncPlayAccess;
+            user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+            user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+            user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+            user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+            user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+            user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+            user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+            user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+            user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+            user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+            user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+            user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+            user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+            user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+            user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+            user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+            user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+            user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+            user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+            user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+            user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+            user.AccessSchedules.Clear();
+            foreach (var policyAccessSchedule in policy.AccessSchedules)
+            {
+                user.AccessSchedules.Add(policyAccessSchedule);
+            }
+
+            // TODO: fix this at some point
+            user.SetPreference(
+                PreferenceKind.BlockUnratedItems,
+                policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
+            user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+            user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+            user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+            user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+            user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+
+            dbContext.Update(user);
+            dbContext.SaveChanges();
+        }
+
+        /// <inheritdoc/>
+        public void ClearProfileImage(User user)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            dbContext.Remove(user.ProfileImage);
+            dbContext.SaveChanges();
+        }
+
+        private static bool IsValidUsername(string name)
+        {
+            // 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(User user)
+        {
+            return GetAuthenticationProviders(user)[0];
+        }
+
+        private IPasswordResetProvider GetPasswordResetProvider(User user)
+        {
+            return GetPasswordResetProviders(user)[0];
+        }
+
+        private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user)
+        {
+            var authenticationProviderId = user?.AuthenticationProviderId;
+
+            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
+
+            if (!string.IsNullOrEmpty(authenticationProviderId))
+            {
+                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+
+            if (providers.Count == 0)
+            {
+                // 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,
+                    user?.AuthenticationProviderId);
+                providers = new List<IAuthenticationProvider>
+                {
+                    _invalidAuthProvider
+                };
+            }
+
+            return providers;
+        }
+
+        private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
+        {
+            var passwordResetProviderId = user?.PasswordResetProviderId;
+            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
+
+            if (!string.IsNullOrEmpty(passwordResetProviderId))
+            {
+                providers = providers.Where(i =>
+                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase))
+                    .ToArray();
+            }
+
+            if (providers.Length == 0)
+            {
+                providers = new IPasswordResetProvider[]
+                {
+                    _defaultPasswordResetProvider
+                };
+            }
+
+            return providers;
+        }
+
+        private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
+                string username,
+                string password,
+                User? user,
+                string remoteEndPoint)
+        {
+            bool success = false;
+            IAuthenticationProvider? authenticationProvider = null;
+
+            foreach (var provider in GetAuthenticationProviders(user))
+            {
+                var providerAuthResult =
+                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
+                var updatedUsername = providerAuthResult.username;
+                success = providerAuthResult.success;
+
+                if (success)
+                {
+                    authenticationProvider = provider;
+                    username = updatedUsername;
+                    break;
+                }
+            }
+
+            if (!success
+                && _networkManager.IsInLocalNetwork(remoteEndPoint)
+                && user?.EnableLocalPassword == true
+                && !string.IsNullOrEmpty(user.EasyPassword))
+            {
+                // Check easy password
+                var passwordHash = PasswordHash.Parse(user.EasyPassword);
+                var hash = _cryptoProvider.ComputeHash(
+                    passwordHash.Id,
+                    Encoding.UTF8.GetBytes(password),
+                    passwordHash.Salt.ToArray());
+                success = passwordHash.Hash.SequenceEqual(hash);
+            }
+
+            return (authenticationProvider, username, success);
+        }
+
+        private async Task<(string username, bool success)> AuthenticateWithProvider(
+            IAuthenticationProvider provider,
+            string username,
+            string password,
+            User? resolvedUser)
+        {
+            try
+            {
+                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
+                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
+                    : await provider.Authenticate(username, password).ConfigureAwait(false);
+
+                if (authenticationResult.Username != username)
+                {
+                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
+                    username = authenticationResult.Username;
+                }
+
+                return (username, true);
+            }
+            catch (AuthenticationException ex)
+            {
+                _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
+
+                return (username, false);
+            }
+        }
+
+        private void IncrementInvalidLoginAttemptCount(User user)
+        {
+            user.InvalidLoginAttemptCount++;
+            int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
+            if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
+            {
+                user.SetPermission(PermissionKind.IsDisabled, true);
+                OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
+                _logger.LogWarning(
+                    "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
+                    user.Username,
+                    user.InvalidLoginAttemptCount);
+            }
+
+            UpdateUser(user);
+        }
+    }
+}

+ 11 - 2
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;
@@ -63,12 +65,15 @@ namespace Jellyfin.Server
 
             // TODO: Set up scoping and use AddDbContextPool
             serviceCollection.AddDbContext<JellyfinDb>(
-                    options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
-                    ServiceLifetime.Transient);
+                options => options
+                    .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
+                    .UseLazyLoadingProxies(),
+                ServiceLifetime.Transient);
 
             serviceCollection.AddSingleton<JellyfinDbProvider>();
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+            serviceCollection.AddSingleton<IUserManager, UserManager>();
 
             base.RegisterServices(serviceCollection);
         }
@@ -79,7 +84,11 @@ namespace Jellyfin.Server
         /// <inheritdoc />
         protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
         {
+            // Jellyfin.Server
             yield return typeof(CoreAppHost).Assembly;
+
+            // Jellyfin.Server.Implementations
+            yield return typeof(JellyfinDb).Assembly;
         }
 
         /// <inheritdoc />

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

@@ -19,7 +19,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.DisableTranscodingThrottling),
             typeof(Routines.CreateUserLoggingConfigFile),
             typeof(Routines.MigrateActivityLogDb),
-            typeof(Routines.RemoveDuplicateExtras)
+            typeof(Routines.RemoveDuplicateExtras),
+            typeof(Routines.MigrateUserDb)
         };
 
         /// <summary>

+ 208 - 0
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -0,0 +1,208 @@
+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.Common.Json;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
+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 const string DbFilename = "users.db";
+
+        private readonly ILogger<MigrateUserDb> _logger;
+        private readonly IServerApplicationPaths _paths;
+        private readonly JellyfinDbProvider _provider;
+        private readonly 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;
+            _provider = provider;
+            _xmlSerializer = xmlSerializer;
+        }
+
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
+
+        /// <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, DbFilename), ConnectionFlags.ReadOnly, null))
+            {
+                var dbContext = _provider.CreateContext();
+
+                var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
+
+                dbContext.RemoveRange(dbContext.Users);
+                dbContext.SaveChanges();
+
+                foreach (var entry in queryResult)
+                {
+                    UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+                    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;
+                    int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+                    {
+                        -1 => null,
+                        0 => 3,
+                        _ => policy.LoginAttemptsBeforeLockout
+                    };
+
+                    var user = new User(mockup.Name, policy.AuthenticationProviderId, policy.PasswordResetProviderId)
+                    {
+                        Id = entry[1].ReadGuidFromBlob(),
+                        InternalId = entry[0].ToInt64(),
+                        MaxParentalAgeRating = policy.MaxParentalRating,
+                        EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
+                        RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
+                        InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
+                        LoginAttemptsBeforeLockout = maxLoginAttempts,
+                        SubtitleMode = config.SubtitleMode,
+                        HidePlayedInLatest = config.HidePlayedInLatest,
+                        EnableLocalPassword = config.EnableLocalPassword,
+                        PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
+                        DisplayCollectionsView = config.DisplayCollectionsView,
+                        DisplayMissingEpisodes = config.DisplayMissingEpisodes,
+                        AudioLanguagePreference = config.AudioLanguagePreference,
+                        RememberAudioSelections = config.RememberAudioSelections,
+                        EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
+                        RememberSubtitleSelections = config.RememberSubtitleSelections,
+                        SubtitleLanguagePreference = config.SubtitleLanguagePreference,
+                        Password = mockup.Password,
+                        EasyPassword = mockup.EasyPassword,
+                        LastLoginDate = mockup.LastLoginDate,
+                        LastActivityDate = mockup.LastActivityDate
+                    };
+
+                    if (mockup.ImageInfos.Length > 0)
+                    {
+                        ItemImageInfo info = mockup.ImageInfos[0];
+
+                        user.ProfileImage = new ImageInfo(info.Path)
+                        {
+                            LastModified = info.DateModified
+                        };
+                    }
+
+                    user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+                    user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+                    user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+                    user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+                    user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+                    user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+                    user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+                    user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+                    user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+                    user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+                    user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+                    user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+                    user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+                    user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+                    user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+                    user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+                    user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+                    user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+                    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);
+                    }
+
+                    user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+                    user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+                    user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+                    user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+                    user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+                    user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+                    user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+                    user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+                    user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+                    dbContext.Users.Add(user);
+                }
+
+                dbContext.SaveChanges();
+            }
+
+            try
+            {
+                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; }
+
+            public ItemImageInfo[] ImageInfos { get; set; }
+        }
+    }
+}

+ 3 - 2
MediaBrowser.Api/BaseApiService.cs

@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -94,8 +95,8 @@ namespace MediaBrowser.Api
             var authenticatedUser = auth.User;
 
             // If they're going to update the record of another user, they must be an administrator
-            if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator)
-                || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess))
+            if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
+                || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
             {
                 throw new SecurityException("Unauthorized access.");
             }

+ 1 - 0
MediaBrowser.Api/FilterService.cs

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

+ 144 - 16
MediaBrowser.Api/Images/ImageService.cs

@@ -18,9 +18,11 @@ using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
+using User = Jellyfin.Data.Entities.User;
 
 namespace MediaBrowser.Api.Images
 {
@@ -408,14 +410,14 @@ namespace MediaBrowser.Api.Images
         {
             var item = _userManager.GetUserById(request.Id);
 
-            return GetImage(request, Guid.Empty, item, false);
+            return GetImage(request, item, false);
         }
 
         public object Head(GetUserImage request)
         {
             var item = _userManager.GetUserById(request.Id);
 
-            return GetImage(request, Guid.Empty, item, true);
+            return GetImage(request, item, true);
         }
 
         public object Get(GetItemByNameImage request)
@@ -448,9 +450,9 @@ namespace MediaBrowser.Api.Images
 
             request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
 
-            var item = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(id);
 
-            return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
+            return PostImage(user, request.RequestStream, Request.ContentType);
         }
 
         /// <summary>
@@ -477,9 +479,17 @@ namespace MediaBrowser.Api.Images
             var userId = request.Id;
             AssertCanUpdateUser(_authContext, _userManager, userId, true);
 
-            var item = _userManager.GetUserById(userId);
+            var user = _userManager.GetUserById(userId);
+            try
+            {
+                File.Delete(user.ProfileImage.Path);
+            }
+            catch (IOException e)
+            {
+                Logger.LogError(e, "Error deleting user profile image:");
+            }
 
-            item.DeleteImage(request.Type, request.Index ?? 0);
+            _userManager.ClearProfileImage(user);
         }
 
         /// <summary>
@@ -567,14 +577,14 @@ namespace MediaBrowser.Api.Images
                 throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
             }
 
-            bool cropwhitespace;
+            bool cropWhitespace;
             if (request.CropWhitespace.HasValue)
             {
-                cropwhitespace = request.CropWhitespace.Value;
+                cropWhitespace = request.CropWhitespace.Value;
             }
             else
             {
-                cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
+                cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
             }
 
             var outputFormats = GetOutputFormats(request);
@@ -597,13 +607,90 @@ namespace MediaBrowser.Api.Images
                 itemId,
                 request,
                 imageInfo,
-                cropwhitespace,
+                cropWhitespace,
+                outputFormats,
+                cacheDuration,
+                responseHeaders,
+                isHeadRequest);
+        }
+
+        public Task<object> GetImage(ImageRequest request, User user, bool isHeadRequest)
+        {
+            var imageInfo = GetImageInfo(request, user);
+
+            TimeSpan? cacheDuration = null;
+
+            if (!string.IsNullOrEmpty(request.Tag))
+            {
+                cacheDuration = TimeSpan.FromDays(365);
+            }
+
+            var responseHeaders = new Dictionary<string, string>
+            {
+                {"transferMode.dlna.org", "Interactive"},
+                {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
+            };
+
+            var outputFormats = GetOutputFormats(request);
+
+            return GetImageResult(user.Id,
+                request,
+                imageInfo,
                 outputFormats,
                 cacheDuration,
                 responseHeaders,
                 isHeadRequest);
         }
 
+        private async Task<object> GetImageResult(
+            Guid itemId,
+            ImageRequest request,
+            ItemImageInfo info,
+            IReadOnlyCollection<ImageFormat> supportedFormats,
+            TimeSpan? cacheDuration,
+            IDictionary<string, string> headers,
+            bool isHeadRequest)
+        {
+            info.Type = ImageType.Profile;
+            var options = new ImageProcessingOptions
+            {
+                CropWhiteSpace = true,
+                Height = request.Height,
+                ImageIndex = request.Index ?? 0,
+                Image = info,
+                Item = null, // Hack alert
+                ItemId = itemId,
+                MaxHeight = request.MaxHeight,
+                MaxWidth = request.MaxWidth,
+                Quality = request.Quality ?? 100,
+                Width = request.Width,
+                AddPlayedIndicator = request.AddPlayedIndicator,
+                PercentPlayed = 0,
+                UnplayedCount = request.UnplayedCount,
+                Blur = request.Blur,
+                BackgroundColor = request.BackgroundColor,
+                ForegroundLayer = request.ForegroundLayer,
+                SupportedOutputFormats = supportedFormats
+            };
+
+            var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
+
+            headers[HeaderNames.Vary] = HeaderNames.Accept;
+
+            return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                CacheDuration = cacheDuration,
+                ResponseHeaders = headers,
+                ContentType = imageResult.Item2,
+                DateLastModified = imageResult.Item3,
+                IsHeadRequest = isHeadRequest,
+                Path = imageResult.Item1,
+
+                FileShare = FileShare.Read
+
+            }).ConfigureAwait(false);
+        }
+
         private async Task<object> GetImageResult(
             BaseItem item,
             Guid itemId,
@@ -741,13 +828,35 @@ namespace MediaBrowser.Api.Images
         /// <param name="request">The request.</param>
         /// <param name="item">The item.</param>
         /// <returns>System.String.</returns>
-        private ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
+        private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
         {
             var index = request.Index ?? 0;
 
             return item.GetImageInfo(request.Type, index);
         }
 
+        private static ItemImageInfo GetImageInfo(ImageRequest request, User user)
+        {
+            var info = new ItemImageInfo
+            {
+                Path = user.ProfileImage.Path,
+                Type = ImageType.Primary,
+                DateModified = user.ProfileImage.LastModified,
+            };
+
+            if (request.Width.HasValue)
+            {
+                info.Width = request.Width.Value;
+            }
+
+            if (request.Height.HasValue)
+            {
+                info.Height = request.Height.Value;
+            }
+
+            return info;
+        }
+
         /// <summary>
         /// Posts the image.
         /// </summary>
@@ -757,23 +866,42 @@ namespace MediaBrowser.Api.Images
         /// <param name="mimeType">Type of the MIME.</param>
         /// <returns>Task.</returns>
         public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
+        {
+            var memoryStream = await GetMemoryStream(inputStream);
+
+            // Handle image/png; charset=utf-8
+            mimeType = mimeType.Split(';').FirstOrDefault();
+
+            await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+
+            entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+        }
+
+        private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
         {
             using var reader = new StreamReader(inputStream);
             var text = await reader.ReadToEndAsync().ConfigureAwait(false);
 
             var bytes = Convert.FromBase64String(text);
-
-            var memoryStream = new MemoryStream(bytes)
+            return new MemoryStream(bytes)
             {
                 Position = 0
             };
+        }
+
+        private async Task PostImage(User user, Stream inputStream, string mimeType)
+        {
+            var memoryStream = await GetMemoryStream(inputStream);
 
             // Handle image/png; charset=utf-8
             mimeType = mimeType.Split(';').FirstOrDefault();
+            var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+            user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
 
-            await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
-
-            entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+            await _providerManager
+                .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
+                .ConfigureAwait(false);
+            await _userManager.UpdateUserAsync(user);
         }
     }
 }

+ 11 - 5
MediaBrowser.Api/Library/LibraryService.cs

@@ -6,6 +6,7 @@ using System.Net;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Api.Movies;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
@@ -14,7 +15,6 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
@@ -27,6 +27,12 @@ using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace MediaBrowser.Api.Library
 {
@@ -350,6 +356,7 @@ namespace MediaBrowser.Api.Library
             _moviesServiceLogger = moviesServiceLogger;
         }
 
+        // Content Types available for each Library
         private string[] GetRepresentativeItemTypes(string contentType)
         {
             return contentType switch
@@ -359,7 +366,7 @@ namespace MediaBrowser.Api.Library
                 CollectionType.Movies => new[] {"Movie"},
                 CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
                 CollectionType.Books => new[] {"Book"},
-                CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"},
+                CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
                 CollectionType.HomeVideos => new[] {"Video", "Photo"},
                 CollectionType.Photos => new[] {"Video", "Photo"},
                 CollectionType.MusicVideos => new[] {"MusicVideo"},
@@ -425,7 +432,6 @@ namespace MediaBrowser.Api.Library
                 return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
                        || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
                        || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)
                        || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
             }
 
@@ -763,8 +769,8 @@ namespace MediaBrowser.Api.Library
         {
             try
             {
-                _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
-                    string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
+                _activityManager.Create(new ActivityLog(
+                    string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
                     "UserDownloadingContent",
                     auth.UserId)
                 {

+ 2 - 1
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -7,6 +7,7 @@ using System.Security.Cryptography;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Api.UserLibrary;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -859,7 +860,7 @@ namespace MediaBrowser.Api.LiveTv
                 throw new SecurityException("Anonymous live tv management is not allowed.");
             }
 
-            if (!user.Policy.EnableLiveTvManagement)
+            if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
             {
                 throw new SecurityException("The current user does not have permission to manage live tv.");
             }

+ 9 - 2
MediaBrowser.Api/Movies/MoviesService.cs

@@ -2,11 +2,11 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
@@ -15,6 +15,8 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
+using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 
 namespace MediaBrowser.Api.Movies
 {
@@ -251,7 +253,12 @@ namespace MediaBrowser.Api.Movies
             return categories.OrderBy(i => i.RecommendationType);
         }
 
-        private IEnumerable<RecommendationDto> GetWithDirector(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+        private IEnumerable<RecommendationDto> GetWithDirector(
+            User user,
+            IEnumerable<string> names,
+            int itemLimit,
+            DtoOptions dtoOptions,
+            RecommendationType type)
         {
             var itemTypes = new List<string> { typeof(Movie).Name };
             if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)

+ 1 - 0
MediaBrowser.Api/Music/InstantMixService.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;

+ 2 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -196,7 +197,7 @@ namespace MediaBrowser.Api.Playback
             if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
             {
                 var auth = AuthorizationContext.GetAuthorizationInfo(Request);
-                if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
+                if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
                 {
                     ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
 

+ 22 - 14
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
@@ -400,21 +401,24 @@ namespace MediaBrowser.Api.Playback
 
             if (item is Audio)
             {
-                Logger.LogInformation("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
+                Logger.LogInformation(
+                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
             }
             else
             {
                 Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
-                    user.Name,
-                    user.Policy.EnablePlaybackRemuxing,
-                    user.Policy.EnableVideoPlaybackTranscoding,
-                    user.Policy.EnableAudioPlaybackTranscoding);
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
             }
 
             // Beginning of Playback Determination: Attempt DirectPlay first
             if (mediaSource.SupportsDirectPlay)
             {
-                if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
                 {
                     mediaSource.SupportsDirectPlay = false;
                 }
@@ -428,14 +432,16 @@ namespace MediaBrowser.Api.Playback
 
                     if (item is Audio)
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding)
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
                         {
                             options.ForceDirectPlay = true;
                         }
                     }
                     else if (item is Video)
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
                         {
                             options.ForceDirectPlay = true;
                         }
@@ -463,7 +469,7 @@ namespace MediaBrowser.Api.Playback
 
             if (mediaSource.SupportsDirectStream)
             {
-                if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
                 {
                     mediaSource.SupportsDirectStream = false;
                 }
@@ -473,14 +479,16 @@ namespace MediaBrowser.Api.Playback
 
                     if (item is Audio)
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding)
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
                         {
                             options.ForceDirectStream = true;
                         }
                     }
                     else if (item is Video)
                     {
-                        if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
                         {
                             options.ForceDirectStream = true;
                         }
@@ -512,7 +520,7 @@ namespace MediaBrowser.Api.Playback
                     ? streamBuilder.BuildAudioItem(options)
                     : streamBuilder.BuildVideoItem(options);
 
-                if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
                 {
                     if (streamInfo != null)
                     {
@@ -576,10 +584,10 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        private long? GetMaxBitrate(long? clientMaxBitrate, User user)
+        private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user)
         {
             var maxBitrate = clientMaxBitrate;
-            var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0;
+            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
 
             if (remoteClientMaxBitrate <= 0)
             {

+ 3 - 2
MediaBrowser.Api/Sessions/SessionService.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
@@ -326,12 +327,12 @@ namespace MediaBrowser.Api.Sessions
 
                 var user = _userManager.GetUserById(request.ControllableByUserId);
 
-                if (!user.Policy.EnableRemoteControlOfOtherUsers)
+                if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
                 {
                     result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
                 }
 
-                if (!user.Policy.EnableSharedDeviceControl)
+                if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
                 {
                     result = result.Where(i => !i.UserId.Equals(Guid.Empty));
                 }

+ 1 - 0
MediaBrowser.Api/SuggestionsService.cs

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

+ 4 - 4
MediaBrowser.Api/TvShowsService.cs

@@ -378,7 +378,7 @@ namespace MediaBrowser.Api
         {
             var user = _userManager.GetUserById(request.UserId);
 
-            var series = GetSeries(request.Id, user);
+            var series = GetSeries(request.Id);
 
             if (series == null)
             {
@@ -404,7 +404,7 @@ namespace MediaBrowser.Api
             };
         }
 
-        private Series GetSeries(string seriesId, User user)
+        private Series GetSeries(string seriesId)
         {
             if (!string.IsNullOrWhiteSpace(seriesId))
             {
@@ -433,7 +433,7 @@ namespace MediaBrowser.Api
             }
             else if (request.Season.HasValue)
             {
-                var series = GetSeries(request.Id, user);
+                var series = GetSeries(request.Id);
 
                 if (series == null)
                 {
@@ -446,7 +446,7 @@ namespace MediaBrowser.Api
             }
             else
             {
-                var series = GetSeries(request.Id, user);
+                var series = GetSeries(request.Id);
 
                 if (series == null)
                 {

+ 1 - 0
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

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

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

@@ -2,10 +2,11 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
@@ -14,6 +15,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
 namespace MediaBrowser.Api.UserLibrary
 {
@@ -86,7 +88,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var ancestorIds = Array.Empty<Guid>();
 
-            var excludeFolderIds = user.Configuration.LatestItemsExcludes;
+            var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
             if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
             {
                 ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
@@ -211,14 +213,14 @@ namespace MediaBrowser.Api.UserLibrary
                 request.IncludeItemTypes = "Playlist";
             }
 
-            bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id)
+            bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
                     // Assume all folders inside an EnabledChannel are enabled
-                    || user.Policy.EnabledChannels.Any(i => new Guid(i) == item.Id);
+                    || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
 
             var collectionFolders = _libraryManager.GetCollectionFolders(item);
             foreach (var collectionFolder in collectionFolders)
             {
-                if (user.Policy.EnabledFolders.Contains(
+                if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
                     collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
                     StringComparer.OrdinalIgnoreCase))
                 {
@@ -226,9 +228,12 @@ namespace MediaBrowser.Api.UserLibrary
                 }
             }
 
-            if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder && !user.Policy.EnableAllChannels)
+            if (!(item is UserRootFolder)
+                && !isInEnabledFolder
+                && !user.HasPermission(PermissionKind.EnableAllFolders)
+                && !user.HasPermission(PermissionKind.EnableAllChannels))
             {
-                Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name);
+                Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
                 return new QueryResult<BaseItem>
                 {
                     Items = Array.Empty<BaseItem>(),

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

@@ -1,8 +1,8 @@
 using System;
 using System.Globalization;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;

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

@@ -312,7 +312,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             if (!request.IsPlayed.HasValue)
             {
-                if (user.Configuration.HidePlayedInLatest)
+                if (user.HidePlayedInLatest)
                 {
                     request.IsPlayed = false;
                 }

+ 21 - 16
MediaBrowser.Api/UserService.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
@@ -300,12 +301,12 @@ namespace MediaBrowser.Api
 
             if (request.IsDisabled.HasValue)
             {
-                users = users.Where(i => i.Policy.IsDisabled == request.IsDisabled.Value);
+                users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value);
             }
 
             if (request.IsHidden.HasValue)
             {
-                users = users.Where(i => i.Policy.IsHidden == request.IsHidden.Value);
+                users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value);
             }
 
             if (filterByDevice)
@@ -322,12 +323,12 @@ namespace MediaBrowser.Api
             {
                 if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
                 {
-                    users = users.Where(i => i.Policy.EnableRemoteAccess);
+                    users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
                 }
             }
 
             var result = users
-                .OrderBy(u => u.Name)
+                .OrderBy(u => u.Username)
                 .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
                 .ToArray();
 
@@ -397,7 +398,7 @@ namespace MediaBrowser.Api
             // Password should always be null
             return Post(new AuthenticateUserByName
             {
-                Username = user.Name,
+                Username = user.Username,
                 Password = null,
                 Pw = request.Pw
             });
@@ -456,7 +457,12 @@ namespace MediaBrowser.Api
             }
             else
             {
-                var success = await _userManager.AuthenticateUser(user.Name, request.CurrentPw, request.CurrentPassword, Request.RemoteIp, false).ConfigureAwait(false);
+                var success = await _userManager.AuthenticateUser(
+                    user.Username,
+                    request.CurrentPw,
+                    request.CurrentPassword,
+                    Request.RemoteIp,
+                    false).ConfigureAwait(false);
 
                 if (success == null)
                 {
@@ -506,10 +512,10 @@ namespace MediaBrowser.Api
 
             var user = _userManager.GetUserById(id);
 
-            if (string.Equals(user.Name, dtoUser.Name, StringComparison.Ordinal))
+            if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal))
             {
-                _userManager.UpdateUser(user);
-                _userManager.UpdateConfiguration(user, dtoUser.Configuration);
+                await _userManager.UpdateUserAsync(user);
+                _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration);
             }
             else
             {
@@ -560,7 +566,6 @@ namespace MediaBrowser.Api
             AssertCanUpdateUser(_authContext, _userManager, request.Id, false);
 
             _userManager.UpdateConfiguration(request.Id, request);
-
         }
 
         public void Post(UpdateUserPolicy request)
@@ -568,24 +573,24 @@ namespace MediaBrowser.Api
             var user = _userManager.GetUserById(request.Id);
 
             // If removing admin access
-            if (!request.IsAdministrator && user.Policy.IsAdministrator)
+            if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
             {
-                if (_userManager.Users.Count(i => i.Policy.IsAdministrator) == 1)
+                if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
                 {
                     throw new ArgumentException("There must be at least one user in the system with administrative access.");
                 }
             }
 
             // If disabling
-            if (request.IsDisabled && user.Policy.IsAdministrator)
+            if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
             {
                 throw new ArgumentException("Administrators cannot be disabled.");
             }
 
             // If disabling
-            if (request.IsDisabled && !user.Policy.IsDisabled)
+            if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
             {
-                if (_userManager.Users.Count(i => !i.Policy.IsDisabled) == 1)
+                if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
                 {
                     throw new ArgumentException("There must be at least one enabled user in the system.");
                 }
@@ -594,7 +599,7 @@ namespace MediaBrowser.Api
                 _sessionMananger.RevokeUserTokens(user.Id, currentToken);
             }
 
-            _userManager.UpdateUserPolicy(request.Id, request);
+            _userManager.UpdatePolicy(request.Id, request);
         }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs

@@ -1,5 +1,5 @@
 using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Authentication

+ 1 - 1
MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Authentication

+ 7 - 3
MediaBrowser.Controller/Channels/Channel.cs

@@ -3,6 +3,8 @@ using System.Globalization;
 using System.Linq;
 using System.Text.Json.Serialization;
 using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Querying;
@@ -13,16 +15,18 @@ namespace MediaBrowser.Controller.Channels
     {
         public override bool IsVisible(User user)
         {
-            if (user.Policy.BlockedChannels != null)
+            if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
             {
-                if (user.Policy.BlockedChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+                if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
                 {
                     return false;
                 }
             }
             else
             {
-                if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+                if (!user.HasPermission(PermissionKind.EnableAllChannels)
+                    && !user.GetPreference(PreferenceKind.EnabledChannels)
+                        .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
                 {
                     return false;
                 }

+ 1 - 0
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 

部分文件因为文件数量过多而无法显示