Przeglądaj źródła

Merge remote-tracking branch 'upstream/master' into livetv-ts-fix

crobibero 4 lat temu
rodzic
commit
5d88e61076
67 zmienionych plików z 515 dodań i 322 usunięć
  1. 16 16
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  2. 1 1
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  3. 2 2
      Emby.Server.Implementations/ApplicationHost.cs
  4. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  5. 1 1
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  6. 17 17
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  7. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  8. 1 1
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  9. 1 1
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  10. 7 2
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  11. 2 2
      Emby.Server.Implementations/Library/MusicManager.cs
  12. 14 14
      Emby.Server.Implementations/Library/SearchEngine.cs
  13. 1 1
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  14. 1 1
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  15. 1 1
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  16. 7 7
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  17. 4 4
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  18. 11 11
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  19. 2 0
      Emby.Server.Implementations/Localization/Core/en-US.json
  20. 1 1
      Emby.Server.Implementations/Localization/Core/ta.json
  21. 4 4
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  22. 78 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
  23. 1 1
      Emby.Server.Implementations/ServerApplicationPaths.cs
  24. 2 2
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  25. 19 47
      Emby.Server.Implementations/Updates/InstallationManager.cs
  26. 9 8
      Jellyfin.Api/Controllers/ArtistsController.cs
  27. 3 2
      Jellyfin.Api/Controllers/ChannelsController.cs
  28. 6 6
      Jellyfin.Api/Controllers/CollectionController.cs
  29. 2 1
      Jellyfin.Api/Controllers/GenresController.cs
  30. 3 3
      Jellyfin.Api/Controllers/ImageController.cs
  31. 3 2
      Jellyfin.Api/Controllers/ItemsController.cs
  32. 2 1
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  33. 2 2
      Jellyfin.Api/Controllers/LiveTvController.cs
  34. 2 2
      Jellyfin.Api/Controllers/MoviesController.cs
  35. 2 1
      Jellyfin.Api/Controllers/MusicGenresController.cs
  36. 2 1
      Jellyfin.Api/Controllers/PersonsController.cs
  37. 1 1
      Jellyfin.Api/Controllers/SessionController.cs
  38. 5 4
      Jellyfin.Api/Controllers/StudiosController.cs
  39. 3 2
      Jellyfin.Api/Controllers/TrailersController.cs
  40. 2 4
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  41. 7 11
      Jellyfin.Api/Controllers/UserController.cs
  42. 2 3
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  43. 162 0
      Jellyfin.Api/Helpers/ProgressiveFileStream.cs
  44. 0 29
      Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs
  45. 12 0
      Jellyfin.Server.Implementations/Activity/ActivityManager.cs
  46. 11 13
      Jellyfin.Server.Implementations/Users/UserManager.cs
  47. 0 3
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  48. 1 1
      Jellyfin.Server/Jellyfin.Server.csproj
  49. 10 20
      Jellyfin.Server/Program.cs
  50. 0 23
      MediaBrowser.Common/Updates/IInstallationManager.cs
  51. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  52. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  53. 2 2
      MediaBrowser.Controller/Entities/Folder.cs
  54. 7 1
      MediaBrowser.Controller/Entities/Genre.cs
  55. 5 5
      MediaBrowser.Controller/Entities/TV/Series.cs
  56. 15 15
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  57. 6 3
      MediaBrowser.Controller/Library/IUserManager.cs
  58. 3 2
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  59. 5 0
      MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
  60. 2 2
      MediaBrowser.Controller/Playlists/Playlist.cs
  61. 7 0
      MediaBrowser.Model/Activity/IActivityManager.cs
  62. 6 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  63. 1 1
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  64. 1 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
  65. 0 1
      debian/control
  66. 3 3
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  67. 2 2
      tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

+ 16 - 16
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
                 User = user,
                 Recursive = true,
                 IsMissing = false,
-                ExcludeItemTypes = new[] { typeof(Book).Name },
+                ExcludeItemTypes = new[] { nameof(Book) },
                 IsFolder = isFolder,
                 MediaTypes = mediaTypes,
                 DtoOptions = GetDtoOptions()
@@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
                 Limit = limit,
                 StartIndex = startIndex,
                 IsVirtualItem = false,
-                ExcludeItemTypes = new[] { typeof(Book).Name },
+                ExcludeItemTypes = new[] { nameof(Book) },
                 IsPlaceHolder = false,
                 DtoOptions = GetDtoOptions()
             };
@@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
                 StartIndex = startIndex,
                 Limit = limit,
             };
-            query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
+            query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
 
             SetSorting(query, sort, false);
 
@@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
             // query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+            query.IncludeItemTypes = new[] { nameof(BoxSet) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+            query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Audio).Name };
+            query.IncludeItemTypes = new[] { nameof(Audio) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Audio).Name };
+            query.IncludeItemTypes = new[] { nameof(Audio) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Episode).Name };
+            query.IncludeItemTypes = new[] { nameof(Episode) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
+            query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
                 {
                     UserId = user.Id,
                     Limit = 50,
-                    IncludeItemTypes = new[] { typeof(Episode).Name },
+                    IncludeItemTypes = new[] { nameof(Episode) },
                     ParentId = parent == null ? Guid.Empty : parent.Id,
                     GroupItems = false
                 },
@@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 ArtistIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                IncludeItemTypes = new[] { nameof(MusicAlbum) },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()
@@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
                 Recursive = true,
                 ParentId = parentId,
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                IncludeItemTypes = new[] { nameof(MusicAlbum) },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions()

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
         }
 
         /// <inheritdoc />
-        public string VirtualDataPath { get; } = "%AppDataPath%";
+        public string VirtualDataPath => "%AppDataPath%";
 
         /// <summary>
         /// Gets the image cache path.

+ 2 - 2
Emby.Server.Implementations/ApplicationHost.cs

@@ -339,7 +339,7 @@ namespace Emby.Server.Implementations
         /// Gets the email address for use within a comment section of a user agent field.
         /// Presently used to provide contact information to MusicBrainz service.
         /// </summary>
-        public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
+        public string ApplicationUserAgentAddress => "team@jellyfin.org";
 
         /// <summary>
         /// Gets the current application name.
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Resolves this instance.
         /// </summary>
-        /// <typeparam name="T">The type</typeparam>
+        /// <typeparam name="T">The type.</typeparam>
         /// <returns>``0.</returns>
         public T Resolve<T>() => ServiceProvider.GetService<T>();
 

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

@@ -543,7 +543,7 @@ namespace Emby.Server.Implementations.Channels
             return _libraryManager.GetItemIds(
                 new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(Channel).Name },
+                    IncludeItemTypes = new[] { nameof(Channel) },
                     OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
                 }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
         }

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
 
             var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Channel).Name },
+                IncludeItemTypes = new[] { nameof(Channel) },
                 ExcludeItemIds = installedChannelIds.ToArray()
             });
 

+ 17 - 17
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3914,7 +3914,7 @@ namespace Emby.Server.Implementations.Data
                 if (query.IsPlayed.HasValue)
                 {
                     // We should probably figure this out for all folders, but for right now, this is the only place where we need it
-                    if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
+                    if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
                     {
                         if (query.IsPlayed.Value)
                         {
@@ -4755,29 +4755,29 @@ namespace Emby.Server.Implementations.Data
         {
             var list = new List<string>();
 
-            if (IsTypeInQuery(typeof(Person).Name, query))
+            if (IsTypeInQuery(nameof(Person), query))
             {
-                list.Add(typeof(Person).Name);
+                list.Add(nameof(Person));
             }
 
-            if (IsTypeInQuery(typeof(Genre).Name, query))
+            if (IsTypeInQuery(nameof(Genre), query))
             {
-                list.Add(typeof(Genre).Name);
+                list.Add(nameof(Genre));
             }
 
-            if (IsTypeInQuery(typeof(MusicGenre).Name, query))
+            if (IsTypeInQuery(nameof(MusicGenre), query))
             {
-                list.Add(typeof(MusicGenre).Name);
+                list.Add(nameof(MusicGenre));
             }
 
-            if (IsTypeInQuery(typeof(MusicArtist).Name, query))
+            if (IsTypeInQuery(nameof(MusicArtist), query))
             {
-                list.Add(typeof(MusicArtist).Name);
+                list.Add(nameof(MusicArtist));
             }
 
-            if (IsTypeInQuery(typeof(Studio).Name, query))
+            if (IsTypeInQuery(nameof(Studio), query))
             {
-                list.Add(typeof(Studio).Name);
+                list.Add(nameof(Studio));
             }
 
             return list;
@@ -4832,12 +4832,12 @@ namespace Emby.Server.Implementations.Data
 
             var types = new[]
             {
-                typeof(Episode).Name,
-                typeof(Video).Name,
-                typeof(Movie).Name,
-                typeof(MusicVideo).Name,
-                typeof(Series).Name,
-                typeof(Season).Name
+                nameof(Episode),
+                nameof(Video),
+                nameof(Movie),
+                nameof(MusicVideo),
+                nameof(Series),
+                nameof(Season)
             };
 
             if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))

+ 1 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
             {
                 var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+                    IncludeItemTypes = new[] { nameof(MusicAlbum) },
                     Name = item.Album,
                     Limit = 1
                 });

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

@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 if (param.Length == 2)
                 {
                     var value = NormalizeValue(param[1].Trim(new[] { '"' }));
-                    result.Add(param[0], value);
+                    result[param[0]] = value;
                 }
             }
 

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

@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
             // return _libraryManager.GetItemList(new InternalItemsQuery
             // {
             //    ArtistIds = new[] { item.Id },
-            //    IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+            //    IncludeItemTypes = new[] { nameof(MusicAlbum) },
             //    OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
             //    Limit = 4,
             //    Recursive = true,

+ 7 - 2
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Genres = new[] { item.Name },
-                IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name },
+                IncludeItemTypes = new[]
+                {
+                    nameof(MusicAlbum),
+                    nameof(MusicVideo),
+                    nameof(Audio)
+                },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,
                 Recursive = true,
@@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Genres = new[] { item.Name },
-                IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
+                IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,
                 Recursive = true,

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

@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
             var genres = item
                .GetRecursiveChildren(user, new InternalItemsQuery(user)
                {
-                   IncludeItemTypes = new[] { typeof(Audio).Name },
+                   IncludeItemTypes = new[] { nameof(Audio) },
                    DtoOptions = dtoOptions
                })
                .Cast<Audio>()
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
         {
             return _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(Audio).Name },
+                IncludeItemTypes = new[] { nameof(Audio) },
 
                 GenreIds = genreIds.ToArray(),
 

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

@@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
             var excludeItemTypes = query.ExcludeItemTypes.ToList();
             var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
 
-            excludeItemTypes.Add(typeof(Year).Name);
-            excludeItemTypes.Add(typeof(Folder).Name);
+            excludeItemTypes.Add(nameof(Year));
+            excludeItemTypes.Add(nameof(Folder));
 
             if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Genre).Name);
-                    AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+                    AddIfMissing(includeItemTypes, nameof(Genre));
+                    AddIfMissing(includeItemTypes, nameof(MusicGenre));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Genre).Name);
-                AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+                AddIfMissing(excludeItemTypes, nameof(Genre));
+                AddIfMissing(excludeItemTypes, nameof(MusicGenre));
             }
 
             if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Person).Name);
+                    AddIfMissing(includeItemTypes, nameof(Person));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Person).Name);
+                AddIfMissing(excludeItemTypes, nameof(Person));
             }
 
             if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(Studio).Name);
+                    AddIfMissing(includeItemTypes, nameof(Studio));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+                AddIfMissing(excludeItemTypes, nameof(Studio));
             }
 
             if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+                    AddIfMissing(includeItemTypes, nameof(MusicArtist));
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+                AddIfMissing(excludeItemTypes, nameof(MusicArtist));
             }
 
-            AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
-            AddIfMissing(excludeItemTypes, typeof(Folder).Name);
+            AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
+            AddIfMissing(excludeItemTypes, nameof(Folder));
             var mediaTypes = query.MediaTypes.ToList();
 
             if (includeItemTypes.Count > 0)

+ 1 - 1
Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(MusicArtist).Name },
+                IncludeItemTypes = new[] { nameof(MusicArtist) },
                 IsDeadArtist = true,
                 IsLocked = false
             }).Cast<MusicArtist>().ToList();

+ 1 - 1
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Person).Name },
+                IncludeItemTypes = new[] { nameof(Person) },
                 IsDeadPerson = true,
                 IsLocked = false
             });

+ 1 - 1
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Studio).Name },
+                IncludeItemTypes = new[] { nameof(Studio) },
                 IsDeadStudio = true,
                 IsLocked = false
             });

+ 7 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                    IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                     Limit = 1,
                     ExternalId = timer.ProgramId,
                     DtoOptions = new DtoOptions(true)
@@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                 Limit = 1,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                 ExternalSeriesId = seriesTimer.SeriesId,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
                             ItemIds = new[] { parent.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
+                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
                             ItemIds = new[] { programInfo.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 var seriesIds = _libraryManager.GetItemIds(
                     new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { typeof(Series).Name },
+                        IncludeItemTypes = new[] { nameof(Series) },
                         Name = program.Name
                     }).ToArray();
 
@@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     var result = _libraryManager.GetItemIds(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        IncludeItemTypes = new[] { nameof(Episode) },
                         ParentIndexNumber = program.SeasonNumber.Value,
                         IndexNumber = program.EpisodeNumber.Value,
                         AncestorIds = seriesIds,

+ 4 - 4
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var program = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { typeof(Series).Name },
+                IncludeItemTypes = new string[] { nameof(Series) },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
@@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 program = _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                    IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                     ExternalSeriesId = programSeriesId,
                     Limit = 1,
                     ImageTypes = new ImageType[] { ImageType.Primary },

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

@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
                 IsKids = query.IsKids,
                 IsSports = query.IsSports,
                 IsSeries = query.IsSeries,
-                IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvChannel) },
                 TopParentIds = new[] { topFolder.Id },
                 IsFavorite = query.IsFavorite,
                 IsLiked = query.IsLiked,
@@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 MinEndDate = query.MinEndDate,
                 MinStartDate = query.MinStartDate,
                 MaxEndDate = query.MaxEndDate,
@@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 IsAiring = query.IsAiring,
                 HasAired = query.HasAired,
                 IsNews = query.IsNews,
@@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
 
             if (cleanDatabase)
             {
-                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
-                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
+                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
+                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
             }
 
             var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
 
                     var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
+                        IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
                         ChannelIds = new Guid[] { currentChannel.Id },
                         DtoOptions = new DtoOptions(true)
                     }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 if (query.IsMovie.Value)
                 {
-                    includeItemTypes.Add(typeof(Movie).Name);
+                    includeItemTypes.Add(nameof(Movie));
                 }
                 else
                 {
-                    excludeItemTypes.Add(typeof(Movie).Name);
+                    excludeItemTypes.Add(nameof(Movie));
                 }
             }
 
@@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 if (query.IsSeries.Value)
                 {
-                    includeItemTypes.Add(typeof(Episode).Name);
+                    includeItemTypes.Add(nameof(Episode));
                 }
                 else
                 {
-                    excludeItemTypes.Add(typeof(Episode).Name);
+                    excludeItemTypes.Add(nameof(Episode));
                 }
             }
 
@@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
                 ChannelIds = channelIds,
                 MaxStartDate = now,
                 MinEndDate = now,

+ 2 - 0
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -95,6 +95,8 @@
     "TasksLibraryCategory": "Library",
     "TasksApplicationCategory": "Application",
     "TasksChannelsCategory": "Internet Channels",
+    "TaskCleanActivityLog": "Clean Activity Log",
+    "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
     "TaskCleanCache": "Clean Cache Directory",
     "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
     "TaskRefreshChapterImages": "Extract Chapter Images",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ta.json

@@ -101,7 +101,7 @@
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
     "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
     "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
-    "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+    "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
     "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
     "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",

+ 4 - 4
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -703,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 MaxRuntimeTicks = info.MaxRuntimeTicks
             };
 
-            if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+            if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
             {
                 if (!info.TimeOfDayTicks.HasValue)
                 {
@@ -717,7 +717,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 };
             }
 
-            if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+            if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
             {
                 if (!info.TimeOfDayTicks.HasValue)
                 {
@@ -737,7 +737,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 };
             }
 
-            if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
+            if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
             {
                 if (!info.IntervalTicks.HasValue)
                 {
@@ -751,7 +751,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 };
             }
 
-            if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+            if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
             {
                 return new StartupTrigger();
             }

+ 78 - 0
Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+    /// <summary>
+    /// Deletes old activity log entries.
+    /// </summary>
+    public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
+    {
+        private readonly ILocalizationManager _localization;
+        private readonly IActivityManager _activityManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
+        /// </summary>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public CleanActivityLogTask(
+            ILocalizationManager localization,
+            IActivityManager activityManager,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _localization = localization;
+            _activityManager = activityManager;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
+
+        /// <inheritdoc />
+        public string Key => "CleanActivityLog";
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
+        /// <inheritdoc />
+        public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
+            if (!retentionDays.HasValue || retentionDays <= 0)
+            {
+                throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
+            }
+
+            var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1);
+            return _activityManager.CleanAsync(startDate);
+        }
+
+        /// <inheritdoc />
+        public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+        {
+            return Enumerable.Empty<TaskTriggerInfo>();
+        }
+    }
+}

+ 1 - 1
Emby.Server.Implementations/ServerApplicationPaths.cs

@@ -104,6 +104,6 @@ namespace Emby.Server.Implementations
         public string InternalMetadataPath { get; set; }
 
         /// <inheritdoc />
-        public string VirtualInternalMetadataPath { get; } = "%MetadataPath%";
+        public string VirtualInternalMetadataPath => "%MetadataPath%";
     }
 }

+ 2 - 2
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.TV
                 .GetItemList(
                     new InternalItemsQuery(user)
                     {
-                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        IncludeItemTypes = new[] { nameof(Episode) },
                         OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
                         SeriesPresentationUniqueKey = presentationUniqueKey,
                         Limit = limit,
@@ -214,7 +214,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.Ascending) },
                     Limit = 1,
                     IsPlayed = false,

+ 19 - 47
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -10,6 +10,7 @@ using System.Runtime.Serialization;
 using System.Security.Cryptography;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Events;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
@@ -17,6 +18,8 @@ using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.System;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
@@ -36,6 +39,7 @@ namespace Emby.Server.Implementations.Updates
         /// </summary>
         private readonly ILogger<InstallationManager> _logger;
         private readonly IApplicationPaths _appPaths;
+        private readonly IEventManager _eventManager;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerConfigurationManager _config;
@@ -65,23 +69,20 @@ namespace Emby.Server.Implementations.Updates
             ILogger<InstallationManager> logger,
             IApplicationHost appHost,
             IApplicationPaths appPaths,
+            IEventManager eventManager,
             IHttpClientFactory httpClientFactory,
             IJsonSerializer jsonSerializer,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
             IZipClient zipClient)
         {
-            if (logger == null)
-            {
-                throw new ArgumentNullException(nameof(logger));
-            }
-
             _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
             _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
 
             _logger = logger;
             _applicationHost = appHost;
             _appPaths = appPaths;
+            _eventManager = eventManager;
             _httpClientFactory = httpClientFactory;
             _jsonSerializer = jsonSerializer;
             _config = config;
@@ -89,27 +90,6 @@ namespace Emby.Server.Implementations.Updates
             _zipClient = zipClient;
         }
 
-        /// <inheritdoc />
-        public event EventHandler<InstallationInfo> PackageInstalling;
-
-        /// <inheritdoc />
-        public event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
-        /// <inheritdoc />
-        public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
-        /// <inheritdoc />
-        public event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
-        /// <inheritdoc />
-        public event EventHandler<IPlugin> PluginUninstalled;
-
-        /// <inheritdoc />
-        public event EventHandler<InstallationInfo> PluginUpdated;
-
-        /// <inheritdoc />
-        public event EventHandler<InstallationInfo> PluginInstalled;
-
         /// <inheritdoc />
         public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
 
@@ -268,11 +248,11 @@ namespace Emby.Server.Implementations.Updates
 
             var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
 
-            PackageInstalling?.Invoke(this, package);
+            await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
 
             try
             {
-                await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
+                var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
 
                 lock (_currentInstallationsLock)
                 {
@@ -280,8 +260,11 @@ namespace Emby.Server.Implementations.Updates
                 }
 
                 _completedInstallationsInternal.Add(package);
+                await _eventManager.PublishAsync(isUpdate
+                    ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
+                    : new PluginInstalledEventArgs(package)).ConfigureAwait(false);
 
-                PackageInstallationCompleted?.Invoke(this, package);
+                _applicationHost.NotifyPendingRestart();
             }
             catch (OperationCanceledException)
             {
@@ -292,7 +275,7 @@ namespace Emby.Server.Implementations.Updates
 
                 _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
 
-                PackageInstallationCancelled?.Invoke(this, package);
+                await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false);
 
                 throw;
             }
@@ -305,11 +288,11 @@ namespace Emby.Server.Implementations.Updates
                     _currentInstallations.Remove(tuple);
                 }
 
-                PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
+                await _eventManager.PublishAsync(new InstallationFailedEventArgs
                 {
                     InstallationInfo = package,
                     Exception = ex
-                });
+                }).ConfigureAwait(false);
 
                 throw;
             }
@@ -326,7 +309,7 @@ namespace Emby.Server.Implementations.Updates
         /// <param name="package">The package.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns><see cref="Task" />.</returns>
-        private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+        private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
         {
             // Set last update time if we were installed before
             IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
@@ -336,20 +319,9 @@ namespace Emby.Server.Implementations.Updates
             await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
 
             // Do plugin-specific processing
-            if (plugin == null)
-            {
-                _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
+            _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
 
-                PluginInstalled?.Invoke(this, package);
-            }
-            else
-            {
-                _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
-
-                PluginUpdated?.Invoke(this, package);
-            }
-
-            _applicationHost.NotifyPendingRestart();
+            return plugin != null;
         }
 
         private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@@ -467,7 +439,7 @@ namespace Emby.Server.Implementations.Updates
                 _config.SaveConfiguration();
             }
 
-            PluginUninstalled?.Invoke(this, plugin);
+            _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
 
             _applicationHost.NotifyPendingRestart();
         }

+ 9 - 8
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -146,9 +147,9 @@ namespace Jellyfin.Api.Controllers
                 NameLessThan = nameLessThan,
                 NameStartsWith = nameStartsWith,
                 NameStartsWithOrGreater = nameStartsWithOrGreater,
-                Tags = RequestHelpers.Split(tags, ',', true),
-                OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
-                Genres = RequestHelpers.Split(genres, ',', true),
+                Tags = RequestHelpers.Split(tags, '|', true),
+                OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+                Genres = RequestHelpers.Split(genres, '|', true),
                 GenreIds = RequestHelpers.GetGuids(genreIds),
                 StudioIds = RequestHelpers.GetGuids(studioIds),
                 Person = person,
@@ -299,7 +300,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -355,9 +356,9 @@ namespace Jellyfin.Api.Controllers
                 NameLessThan = nameLessThan,
                 NameStartsWith = nameStartsWith,
                 NameStartsWithOrGreater = nameStartsWithOrGreater,
-                Tags = RequestHelpers.Split(tags, ',', true),
-                OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
-                Genres = RequestHelpers.Split(genres, ',', true),
+                Tags = RequestHelpers.Split(tags, '|', true),
+                OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+                Genres = RequestHelpers.Split(genres, '|', true),
                 GenreIds = RequestHelpers.GetGuids(genreIds),
                 StudioIds = RequestHelpers.GetGuids(studioIds),
                 Person = person,

+ 3 - 2
Jellyfin.Api/Controllers/ChannelsController.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? sortOrder,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] string? sortBy,
             [FromQuery] string? fields)
         {
@@ -196,7 +197,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] string? fields,
             [FromQuery] string? channelIds)
         {

+ 6 - 6
Jellyfin.Api/Controllers/CollectionController.cs

@@ -83,14 +83,14 @@ namespace Jellyfin.Api.Controllers
         /// Adds items to a collection.
         /// </summary>
         /// <param name="collectionId">The collection id.</param>
-        /// <param name="itemIds">Item ids, comma delimited.</param>
+        /// <param name="ids">Item ids, comma delimited.</param>
         /// <response code="204">Items added to collection.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+        public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
         {
-            await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
+            await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
             return NoContent();
         }
 
@@ -98,14 +98,14 @@ namespace Jellyfin.Api.Controllers
         /// Removes items from a collection.
         /// </summary>
         /// <param name="collectionId">The collection id.</param>
-        /// <param name="itemIds">Item ids, comma delimited.</param>
+        /// <param name="ids">Item ids, comma delimited.</param>
         /// <response code="204">Items removed from collection.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpDelete("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
+        public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
         {
-            await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
+            await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
             return NoContent();
         }
     }

+ 2 - 1
Jellyfin.Api/Controllers/GenresController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,

+ 3 - 3
Jellyfin.Api/Controllers/ImageController.cs

@@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
             var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
             if (user.ProfileImage != null)
             {
-                _userManager.ClearProfileImage(user);
+                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
             }
 
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
-        public ActionResult DeleteUserImage(
+        public async Task<ActionResult> DeleteUserImage(
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] ImageType imageType,
             [FromRoute] int? index = null)
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
                 _logger.LogError(e, "Error deleting user profile image:");
             }
 
-            _userManager.ClearProfileImage(user);
+            await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
             return NoContent();
         }
 

+ 3 - 2
Jellyfin.Api/Controllers/ItemsController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isHd,
             [FromQuery] bool? is4K,
             [FromQuery] string? locationTypes,
-            [FromQuery] LocationType[] excludeLocationTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isUnaired,
             [FromQuery] double? minCommunityRating,
@@ -182,7 +183,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] ImageType[] imageTypes,

+ 2 - 1
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryStructureDto;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
@@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> AddVirtualFolder(
             [FromQuery] string? name,
             [FromQuery] string? collectionType,
-            [FromQuery] string[] paths,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
             [FromBody] AddVirtualFolderDto? libraryOptionsDto,
             [FromQuery] bool refreshLibrary = false)
         {

+ 2 - 2
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -592,7 +592,7 @@ namespace Jellyfin.Api.Controllers
                 IsKids = isKids,
                 IsSports = isSports,
                 SeriesTimerId = seriesTimerId,
-                Genres = RequestHelpers.Split(genres, ',', true),
+                Genres = RequestHelpers.Split(genres, '|', true),
                 GenreIds = RequestHelpers.GetGuids(genreIds)
             };
 
@@ -648,7 +648,7 @@ namespace Jellyfin.Api.Controllers
                 IsKids = body.IsKids,
                 IsSports = body.IsSports,
                 SeriesTimerId = body.SeriesTimerId,
-                Genres = RequestHelpers.Split(body.Genres, ',', true),
+                Genres = RequestHelpers.Split(body.Genres, '|', true),
                 GenreIds = RequestHelpers.GetGuids(body.GenreIds)
             };
 

+ 2 - 2
Jellyfin.Api/Controllers/MoviesController.cs

@@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
                 IncludeItemTypes = new[]
                 {
                     nameof(Movie),
-                    // typeof(Trailer).Name,
-                    // typeof(LiveTvProgram).Name
+                    // nameof(Trailer),
+                    // nameof(LiveTvProgram)
                 },
                 // IsMovie = true
                 OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),

+ 2 - 1
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,

+ 2 - 1
Jellyfin.Api/Controllers/PersonsController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,

+ 1 - 1
Jellyfin.Api/Controllers/SessionController.cs

@@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult PostCapabilities(
             [FromQuery] string? id,
             [FromQuery] string? playableMediaTypes,
-            [FromQuery] GeneralCommandType[] supportedCommands,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
             [FromQuery] bool supportsMediaControl = false,
             [FromQuery] bool supportsSync = false,
             [FromQuery] bool supportsPersistentIdentifier = true)

+ 5 - 4
Jellyfin.Api/Controllers/StudiosController.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -145,9 +146,9 @@ namespace Jellyfin.Api.Controllers
                 NameLessThan = nameLessThan,
                 NameStartsWith = nameStartsWith,
                 NameStartsWithOrGreater = nameStartsWithOrGreater,
-                Tags = RequestHelpers.Split(tags, ',', true),
-                OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
-                Genres = RequestHelpers.Split(genres, ',', true),
+                Tags = RequestHelpers.Split(tags, '|', true),
+                OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+                Genres = RequestHelpers.Split(genres, '|', true),
                 GenreIds = RequestHelpers.GetGuids(genreIds),
                 StudioIds = RequestHelpers.GetGuids(studioIds),
                 Person = person,

+ 3 - 2
Jellyfin.Api/Controllers/TrailersController.cs

@@ -1,5 +1,6 @@
 using System;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -125,7 +126,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isHd,
             [FromQuery] bool? is4K,
             [FromQuery] string? locationTypes,
-            [FromQuery] LocationType[] excludeLocationTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isUnaired,
             [FromQuery] double? minCommunityRating,
@@ -147,7 +148,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? parentId,
             [FromQuery] string? fields,
             [FromQuery] string? excludeItemTypes,
-            [FromQuery] ItemFilter[] filters,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] ImageType[] imageTypes,

+ 2 - 4
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -88,16 +88,14 @@ namespace Jellyfin.Api.Controllers
         /// <response code="302">Redirected to remote audio stream.</response>
         /// <returns>A <see cref="Task"/> containing the audio file.</returns>
         [HttpGet("Audio/{itemId}/universal")]
-        [HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
         [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
-        [HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status302Found)]
         [ProducesAudioFile]
         public async Task<ActionResult> GetUniversalAudioStream(
             [FromRoute, Required] Guid itemId,
-            [FromRoute] string? container,
+            [FromQuery] string? container,
             [FromQuery] string? mediaSourceId,
             [FromQuery] string? deviceId,
             [FromQuery] Guid? userId,
@@ -276,7 +274,7 @@ namespace Jellyfin.Api.Controllers
 
             foreach (var cont in containers)
             {
-                var parts = RequestHelpers.Split(cont, ',', true);
+                var parts = RequestHelpers.Split(cont, '|', true);
 
                 var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
 

+ 7 - 11
Jellyfin.Api/Controllers/UserController.cs

@@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
 
             var user = _userManager.GetUserById(userId);
 
-            if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
-            {
-                await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
-                _userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
-            }
-            else
+            if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
             {
                 await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
-                _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
             }
 
+            await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
+
             return NoContent();
         }
 
@@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
-        public ActionResult UpdateUserPolicy(
+        public async Task<ActionResult> UpdateUserPolicy(
             [FromRoute, Required] Guid userId,
             [FromBody] UserPolicy newPolicy)
         {
@@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
                 _sessionManager.RevokeUserTokens(user.Id, currentToken);
             }
 
-            _userManager.UpdatePolicy(userId, newPolicy);
+            await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
 
             return NoContent();
         }
@@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
-        public ActionResult UpdateUserConfiguration(
+        public async Task<ActionResult> UpdateUserConfiguration(
             [FromRoute, Required] Guid userId,
             [FromBody] UserConfiguration userConfig)
         {
@@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
                 return Forbid("User configuration update not allowed");
             }
 
-            _userManager.UpdateConfiguration(userId, userConfig);
+            await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
 
             return NoContent();
         }

+ 2 - 3
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -123,9 +123,8 @@ namespace Jellyfin.Api.Helpers
                     state.Dispose();
                 }
 
-                await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
-                    .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
-                return new FileStreamResult(httpContext.Response.Body, contentType);
+                var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
+                return new FileStreamResult(stream, contentType);
             }
             finally
             {

+ 162 - 0
Jellyfin.Api/Helpers/ProgressiveFileStream.cs

@@ -0,0 +1,162 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// A progressive file stream for transferring transcoded files as they are written to.
+    /// </summary>
+    public class ProgressiveFileStream : Stream
+    {
+        private readonly FileStream _fileStream;
+        private readonly TranscodingJobDto? _job;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly bool _allowAsyncFileRead;
+        private int _bytesWritten;
+        private bool _disposed;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+        /// </summary>
+        /// <param name="filePath">The path to the transcoded file.</param>
+        /// <param name="job">The transcoding job information.</param>
+        /// <param name="transcodingJobHelper">The transcoding job helper.</param>
+        public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
+        {
+            _job = job;
+            _transcodingJobHelper = transcodingJobHelper;
+            _bytesWritten = 0;
+
+            var fileOptions = FileOptions.SequentialScan;
+            _allowAsyncFileRead = false;
+
+            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                fileOptions |= FileOptions.Asynchronous;
+                _allowAsyncFileRead = true;
+            }
+
+            _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+        }
+
+        /// <inheritdoc />
+        public override bool CanRead => _fileStream.CanRead;
+
+        /// <inheritdoc />
+        public override bool CanSeek => false;
+
+        /// <inheritdoc />
+        public override bool CanWrite => false;
+
+        /// <inheritdoc />
+        public override long Length => throw new NotSupportedException();
+
+        /// <inheritdoc />
+        public override long Position
+        {
+            get => throw new NotSupportedException();
+            set => throw new NotSupportedException();
+        }
+
+        /// <inheritdoc />
+        public override void Flush()
+        {
+            _fileStream.Flush();
+        }
+
+        /// <inheritdoc />
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            return _fileStream.Read(buffer, offset, count);
+        }
+
+        /// <inheritdoc />
+        public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            int totalBytesRead = 0;
+            int remainingBytesToRead = count;
+
+            while (remainingBytesToRead > 0)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                int bytesRead;
+                if (_allowAsyncFileRead)
+                {
+                    bytesRead = await _fileStream.ReadAsync(buffer, offset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    bytesRead = _fileStream.Read(buffer, offset, remainingBytesToRead);
+                }
+
+                remainingBytesToRead -= bytesRead;
+                if (bytesRead > 0)
+                {
+                    _bytesWritten += bytesRead;
+                    totalBytesRead += bytesRead;
+
+                    if (_job != null)
+                    {
+                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                    }
+                }
+                else
+                {
+                    if (_job == null || _job.HasExited)
+                    {
+                        break;
+                    }
+
+                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                }
+            }
+
+            return totalBytesRead;
+        }
+
+        /// <inheritdoc />
+        public override long Seek(long offset, SeekOrigin origin)
+            => throw new NotSupportedException();
+
+        /// <inheritdoc />
+        public override void SetLength(long value)
+            => throw new NotSupportedException();
+
+        /// <inheritdoc />
+        public override void Write(byte[] buffer, int offset, int count)
+            => throw new NotSupportedException();
+
+        /// <inheritdoc />
+        protected override void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                if (disposing)
+                {
+                    _fileStream.Dispose();
+
+                    if (_job != null)
+                    {
+                        _transcodingJobHelper.OnTranscodeEndRequest(_job);
+                    }
+                }
+            }
+            finally
+            {
+                _disposed = true;
+                base.Dispose(disposing);
+            }
+        }
+    }
+}

+ 0 - 29
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs

@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
-
-namespace Jellyfin.Api.ModelBinders
-{
-    /// <summary>
-    /// Comma delimited array model binder provider.
-    /// </summary>
-    public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
-    {
-        private readonly IModelBinder _binder;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinderProvider"/> class.
-        /// </summary>
-        public CommaDelimitedArrayModelBinderProvider()
-        {
-            _binder = new CommaDelimitedArrayModelBinder();
-        }
-
-        /// <inheritdoc />
-        public IModelBinder? GetBinder(ModelBinderProviderContext context)
-        {
-            return context.Metadata.ModelType.IsArray ? _binder : null;
-        }
-    }
-}

+ 12 - 0
Jellyfin.Server.Implementations/Activity/ActivityManager.cs

@@ -72,6 +72,18 @@ namespace Jellyfin.Server.Implementations.Activity
             };
         }
 
+        /// <inheritdoc />
+        public async Task CleanAsync(DateTime startDate)
+        {
+            await using var dbContext = _provider.CreateContext();
+            var entries = dbContext.ActivityLogs
+                .AsQueryable()
+                .Where(entry => entry.DateCreated <= startDate);
+
+            dbContext.RemoveRange(entries);
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+        }
+
         private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
         {
             return new ActivityLogEntry

+ 11 - 13
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -432,11 +432,9 @@ namespace Jellyfin.Server.Implementations.Users
                     // the authentication provider might have created it
                     user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
 
-                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
+                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user != null)
                     {
-                        UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
-
-                        await UpdateUserAsync(user).ConfigureAwait(false);
+                        await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
                     }
                 }
             }
@@ -615,9 +613,9 @@ namespace Jellyfin.Server.Implementations.Users
         }
 
         /// <inheritdoc/>
-        public void UpdateConfiguration(Guid userId, UserConfiguration config)
+        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
         {
-            using var dbContext = _dbProvider.CreateContext();
+            await using var dbContext = _dbProvider.CreateContext();
             var user = dbContext.Users
                            .Include(u => u.Permissions)
                            .Include(u => u.Preferences)
@@ -644,13 +642,13 @@ namespace Jellyfin.Server.Implementations.Users
             user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 
             dbContext.Update(user);
-            dbContext.SaveChanges();
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
-        public void UpdatePolicy(Guid userId, UserPolicy policy)
+        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
         {
-            using var dbContext = _dbProvider.CreateContext();
+            await using var dbContext = _dbProvider.CreateContext();
             var user = dbContext.Users
                            .Include(u => u.Permissions)
                            .Include(u => u.Preferences)
@@ -715,15 +713,15 @@ namespace Jellyfin.Server.Implementations.Users
             user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
 
             dbContext.Update(user);
-            dbContext.SaveChanges();
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
-        public void ClearProfileImage(User user)
+        public async Task ClearProfileImageAsync(User user)
         {
-            using var dbContext = _dbProvider.CreateContext();
+            await using var dbContext = _dbProvider.CreateContext();
             dbContext.Remove(user.ProfileImage);
-            dbContext.SaveChanges();
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
             user.ProfileImage = null;
         }
 

+ 0 - 3
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -16,7 +16,6 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
-using Jellyfin.Api.ModelBinders;
 using Jellyfin.Server.Configuration;
 using Jellyfin.Server.Filters;
 using Jellyfin.Server.Formatters;
@@ -167,8 +166,6 @@ namespace Jellyfin.Server.Extensions
 
                     opts.OutputFormatters.Add(new CssOutputFormatter());
                     opts.OutputFormatters.Add(new XmlOutputFormatter());
-
-                    opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up

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

@@ -43,7 +43,7 @@
     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
     <PackageReference Include="prometheus-net" Version="4.0.0" />
-    <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
+    <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
     <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
     <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />

+ 10 - 20
Jellyfin.Server/Program.cs

@@ -290,23 +290,19 @@ namespace Jellyfin.Server
                         {
                             _logger.LogInformation("Kestrel listening on {IpAddress}", address);
                             options.Listen(address, appHost.HttpPort);
+
                             if (appHost.ListenWithHttps)
                             {
-                                options.Listen(address, appHost.HttpsPort, listenOptions =>
-                                {
-                                    listenOptions.UseHttps(appHost.Certificate);
-                                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                                });
+                                options.Listen(
+                                    address,
+                                    appHost.HttpsPort,
+                                    listenOptions => listenOptions.UseHttps(appHost.Certificate));
                             }
                             else if (builderContext.HostingEnvironment.IsDevelopment())
                             {
                                 try
                                 {
-                                    options.Listen(address, appHost.HttpsPort, listenOptions =>
-                                    {
-                                        listenOptions.UseHttps();
-                                        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                                    });
+                                    options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
                                 }
                                 catch (InvalidOperationException ex)
                                 {
@@ -322,21 +318,15 @@ namespace Jellyfin.Server
 
                         if (appHost.ListenWithHttps)
                         {
-                            options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
-                            {
-                                listenOptions.UseHttps(appHost.Certificate);
-                                listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                            });
+                            options.ListenAnyIP(
+                                appHost.HttpsPort,
+                                listenOptions => listenOptions.UseHttps(appHost.Certificate));
                         }
                         else if (builderContext.HostingEnvironment.IsDevelopment())
                         {
                             try
                             {
-                                options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
-                                {
-                                    listenOptions.UseHttps();
-                                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                                });
+                                options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
                             }
                             catch (InvalidOperationException ex)
                             {

+ 0 - 23
MediaBrowser.Common/Updates/IInstallationManager.cs

@@ -11,29 +11,6 @@ namespace MediaBrowser.Common.Updates
 {
     public interface IInstallationManager : IDisposable
     {
-        event EventHandler<InstallationInfo> PackageInstalling;
-
-        event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
-        event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
-        event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
-        /// <summary>
-        /// Occurs when a plugin is uninstalled.
-        /// </summary>
-        event EventHandler<IPlugin> PluginUninstalled;
-
-        /// <summary>
-        /// Occurs when a plugin is updated.
-        /// </summary>
-        event EventHandler<InstallationInfo> PluginUpdated;
-
-        /// <summary>
-        /// Occurs when a plugin is installed.
-        /// </summary>
-        event EventHandler<InstallationInfo> PluginInstalled;
-
         /// <summary>
         /// Gets the completed installations.
         /// </summary>

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         {
             if (query.IncludeItemTypes.Length == 0)
             {
-                query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+                query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
                 query.ArtistIds = new[] { Id };
             }
 

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             query.GenreIds = new[] { Id };
-            query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+            query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
 
             return LibraryManager.GetItemList(query);
         }

+ 2 - 2
MediaBrowser.Controller/Entities/Folder.cs

@@ -723,7 +723,7 @@ namespace MediaBrowser.Controller.Entities
 
         private bool RequiresPostFiltering2(InternalItemsQuery query)
         {
-            if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+            if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
             {
                 Logger.LogDebug("Query requires post-filtering due to BoxSet query");
                 return true;
@@ -813,7 +813,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (query.IsPlayed.HasValue)
             {
-                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
                 {
                     Logger.LogDebug("Query requires post-filtering due to IsPlayed");
                     return true;

+ 7 - 1
MediaBrowser.Controller/Entities/Genre.cs

@@ -59,7 +59,13 @@ namespace MediaBrowser.Controller.Entities
         public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             query.GenreIds = new[] { Id };
-            query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+            query.ExcludeItemTypes = new[]
+            {
+                nameof(MusicVideo),
+                nameof(Entities.Audio.Audio),
+                nameof(MusicAlbum),
+                nameof(MusicArtist)
+            };
 
             return LibraryManager.GetItemList(query);
         }

+ 5 - 5
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -151,7 +151,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
             if (query.IncludeItemTypes.Length == 0)
             {
-                query.IncludeItemTypes = new[] { typeof(Episode).Name };
+                query.IncludeItemTypes = new[] { nameof(Episode) };
             }
 
             query.IsVirtualItem = false;
@@ -207,7 +207,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
             query.AncestorWithPresentationUniqueKey = null;
             query.SeriesPresentationUniqueKey = seriesKey;
-            query.IncludeItemTypes = new[] { typeof(Season).Name };
+            query.IncludeItemTypes = new[] { nameof(Season) };
             query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
 
             if (user != null && !user.DisplayMissingEpisodes)
@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
                 if (query.IncludeItemTypes.Length == 0)
                 {
-                    query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name };
+                    query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
                 }
 
                 query.IsVirtualItem = false;
@@ -253,7 +253,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
-                IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
+                IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
                 OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
                 DtoOptions = options
             };
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
                 SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
-                IncludeItemTypes = new[] { typeof(Episode).Name },
+                IncludeItemTypes = new[] { nameof(Episode) },
                 OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
                 DtoOptions = options
             };

+ 15 - 15
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -142,7 +142,7 @@ namespace MediaBrowser.Controller.Entities
 
                 if (query.IncludeItemTypes.Length == 0)
                 {
-                    query.IncludeItemTypes = new[] { typeof(Movie).Name };
+                    query.IncludeItemTypes = new[] { nameof(Movie) };
                 }
 
                 return parent.QueryRecursive(query);
@@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -178,7 +178,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -189,7 +189,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { typeof(Episode).Name };
+            query.IncludeItemTypes = new[] { nameof(Episode) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Entities
         private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
         {
             query.Parent = null;
-            query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+            query.IncludeItemTypes = new[] { nameof(BoxSet) };
             query.SetUser(user);
             query.Recursive = true;
 
@@ -223,7 +223,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -255,7 +255,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var genres = parent.QueryRecursive(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(Movie).Name },
+                IncludeItemTypes = new[] { nameof(Movie) },
                 Recursive = true,
                 EnableTotalRecordCount = false
             }).Items
@@ -286,7 +286,7 @@ namespace MediaBrowser.Controller.Entities
             query.GenreIds = new[] { displayParent.Id };
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Movie).Name };
+            query.IncludeItemTypes = new[] { nameof(Movie) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { typeof(Episode).Name };
+            query.IncludeItemTypes = new[] { nameof(Episode) };
             query.IsVirtualItem = false;
 
             return ConvertToResult(_libraryManager.GetItemList(query));
@@ -362,7 +362,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { typeof(Episode).Name };
+            query.IncludeItemTypes = new[] { nameof(Episode) };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -373,7 +373,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -382,7 +382,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var genres = parent.QueryRecursive(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { typeof(Series).Name },
+                IncludeItemTypes = new[] { nameof(Series) },
                 Recursive = true,
                 EnableTotalRecordCount = false
             }).Items
@@ -413,7 +413,7 @@ namespace MediaBrowser.Controller.Entities
             query.GenreIds = new[] { displayParent.Id };
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { typeof(Series).Name };
+            query.IncludeItemTypes = new[] { nameof(Series) };
 
             return _libraryManager.GetItemsResult(query);
         }

+ 6 - 3
MediaBrowser.Controller/Library/IUserManager.cs

@@ -158,7 +158,8 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="userId">The user's Id.</param>
         /// <param name="config">The request containing the new user configuration.</param>
-        void UpdateConfiguration(Guid userId, UserConfiguration config);
+        /// <returns>A task representing the update.</returns>
+        Task UpdateConfigurationAsync(Guid userId, UserConfiguration config);
 
         /// <summary>
         /// This method updates the user's policy.
@@ -167,12 +168,14 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="userId">The user's Id.</param>
         /// <param name="policy">The request containing the new user policy.</param>
-        void UpdatePolicy(Guid userId, UserPolicy policy);
+        /// <returns>A task representing the update.</returns>
+        Task UpdatePolicyAsync(Guid userId, UserPolicy policy);
 
         /// <summary>
         /// Clears the user's profile image.
         /// </summary>
         /// <param name="user">The user.</param>
-        void ClearProfileImage(User user);
+        /// <returns>A task representing the clearing of the profile image.</returns>
+        Task ClearProfileImageAsync(User user);
     }
 }

+ 3 - 2
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -2675,9 +2675,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             state.MediaSource = mediaSource;
 
             var request = state.BaseRequest;
-            if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+            var supportedAudioCodecs = state.SupportedAudioCodecs;
+            if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0)
             {
-                var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+                var supportedAudioCodecsList = supportedAudioCodecs.ToList();
 
                 ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
 

+ 5 - 0
MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs

@@ -287,6 +287,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 return BaseRequest.AudioChannels;
             }
 
+            if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+            {
+                return BaseRequest.TranscodingMaxAudioChannels;
+            }
+
             if (!string.IsNullOrEmpty(codec))
             {
                 var value = BaseRequest.GetOption(codec, "audiochannels");

+ 2 - 2
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -160,7 +160,7 @@ namespace MediaBrowser.Controller.Playlists
                 return LibraryManager.GetItemList(new InternalItemsQuery(user)
                 {
                     Recursive = true,
-                    IncludeItemTypes = new[] { typeof(Audio).Name },
+                    IncludeItemTypes = new[] { nameof(Audio) },
                     GenreIds = new[] { musicGenre.Id },
                     OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
                     DtoOptions = options
@@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Playlists
                 return LibraryManager.GetItemList(new InternalItemsQuery(user)
                 {
                     Recursive = true,
-                    IncludeItemTypes = new[] { typeof(Audio).Name },
+                    IncludeItemTypes = new[] { nameof(Audio) },
                     ArtistIds = new[] { musicArtist.Id },
                     OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
                     DtoOptions = options

+ 7 - 0
MediaBrowser.Model/Activity/IActivityManager.cs

@@ -16,5 +16,12 @@ namespace MediaBrowser.Model.Activity
         Task CreateAsync(ActivityLog entry);
 
         Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query);
+
+        /// <summary>
+        /// Remove all activity logs before the specified date.
+        /// </summary>
+        /// <param name="startDate">Activity log start date.</param>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+        Task CleanAsync(DateTime startDate);
     }
 }

+ 6 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -271,6 +271,11 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         public string[] KnownProxies { get; set; }
 
+        /// <summary>
+        /// Gets or sets the number of days we should retain activity logs.
+        /// </summary>
+        public int? ActivityLogRetentionDays { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -381,6 +386,7 @@ namespace MediaBrowser.Model.Configuration
             SlowResponseThresholdMs = 500;
             CorsHosts = new[] { "*" };
             KnownProxies = Array.Empty<string>();
+            ActivityLogRetentionDays = 30;
         }
     }
 

+ 1 - 1
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -19,7 +19,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
-    <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
+    <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
     <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
     <PackageReference Include="TvDbSharper" Version="3.2.2" />

+ 1 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs

@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
         {
             var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { typeof(Series).Name },
+                IncludeItemTypes = new[] { nameof(Series) },
                 PersonIds = new[] { item.Id },
                 DtoOptions = new DtoOptions(false)
                 {

+ 0 - 1
debian/control

@@ -20,7 +20,6 @@ Breaks: jellyfin (<<10.6.0)
 Architecture: any
 Depends: at,
          libsqlite3-0,
-         jellyfin-ffmpeg (>= 4.2.1-2),
          libfontconfig1,
          libfreetype6,
          libssl1.1

+ 3 - 3
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -13,9 +13,9 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="AutoFixture" Version="4.13.0" />
-    <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
-    <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
+    <PackageReference Include="AutoFixture" Version="4.14.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
+    <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />

+ 2 - 2
tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

@@ -14,8 +14,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="AutoFixture" Version="4.13.0" />
-    <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
+    <PackageReference Include="AutoFixture" Version="4.14.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
     <PackageReference Include="Moq" Version="4.14.7" />
     <PackageReference Include="xunit" Version="2.4.1" />