From 1180b9746fe7c4a6562baff77910819a6706510b Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 15 Apr 2020 00:01:31 -0600
Subject: [PATCH 001/463] Migrates the notifications service to use ASP.NET MVC
 framework

---
 .../Api/NotificationsService.cs               | 189 ------------------
 .../Controllers/NotificationsController.cs    | 138 +++++++++++++
 .../NotificationDtos/NotificationDto.cs       |  51 +++++
 .../NotificationsSummaryDto.cs                |  20 ++
 .../ApiServiceCollectionExtensions.cs         |   1 +
 5 files changed, 210 insertions(+), 189 deletions(-)
 delete mode 100644 Emby.Notifications/Api/NotificationsService.cs
 create mode 100644 Jellyfin.Api/Controllers/NotificationsController.cs
 create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
 create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs

diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs
deleted file mode 100644
index 788750796d..0000000000
--- a/Emby.Notifications/Api/NotificationsService.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Notifications.Api
-{
-    [Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
-    public class GetNotifications : IReturn<NotificationResult>
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsRead { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-    }
-
-    public class Notification
-    {
-        public string Id { get; set; } = string.Empty;
-
-        public string UserId { get; set; } = string.Empty;
-
-        public DateTime Date { get; set; }
-
-        public bool IsRead { get; set; }
-
-        public string Name { get; set; } = string.Empty;
-
-        public string Description { get; set; } = string.Empty;
-
-        public string Url { get; set; } = string.Empty;
-
-        public NotificationLevel Level { get; set; }
-    }
-
-    public class NotificationResult
-    {
-        public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
-
-        public int TotalRecordCount { get; set; }
-    }
-
-    public class NotificationsSummary
-    {
-        public int UnreadCount { get; set; }
-
-        public NotificationLevel MaxUnreadNotificationLevel { get; set; }
-    }
-
-    [Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
-    public class GetNotificationsSummary : IReturn<NotificationsSummary>
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; } = string.Empty;
-    }
-
-    [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
-    public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
-    {
-    }
-
-    [Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
-    public class GetNotificationServices : IReturn<List<NameIdPair>>
-    {
-    }
-
-    [Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
-    public class AddAdminNotification : IReturnVoid
-    {
-        [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Description { get; set; } = string.Empty;
-
-        [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string? ImageUrl { get; set; }
-
-        [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string? Url { get; set; }
-
-        [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public NotificationLevel Level { get; set; }
-    }
-
-    [Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
-    public class MarkRead : IReturnVoid
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; } = string.Empty;
-    }
-
-    [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
-    public class MarkUnread : IReturnVoid
-    {
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; } = string.Empty;
-
-        [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; } = string.Empty;
-    }
-
-    [Authenticated]
-    public class NotificationsService : IService
-    {
-        private readonly INotificationManager _notificationManager;
-        private readonly IUserManager _userManager;
-
-        public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
-        {
-            _notificationManager = notificationManager;
-            _userManager = userManager;
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationTypes request)
-        {
-            return _notificationManager.GetNotificationTypes();
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationServices request)
-        {
-            return _notificationManager.GetNotificationServices().ToList();
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotificationsSummary request)
-        {
-            return new NotificationsSummary
-            {
-            };
-        }
-
-        public Task Post(AddAdminNotification request)
-        {
-            // This endpoint really just exists as post of a real with sickbeard
-            var notification = new NotificationRequest
-            {
-                Date = DateTime.UtcNow,
-                Description = request.Description,
-                Level = request.Level,
-                Name = request.Name,
-                Url = request.Url,
-                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray()
-            };
-
-            return _notificationManager.SendNotification(notification, CancellationToken.None);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public void Post(MarkRead request)
-        {
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public void Post(MarkUnread request)
-        {
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetNotifications request)
-        {
-            return new NotificationResult();
-        }
-    }
-}
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
new file mode 100644
index 0000000000..6602fca9c7
--- /dev/null
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -0,0 +1,138 @@
+#nullable enable
+#pragma warning disable CA1801
+#pragma warning disable SA1313
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Models.NotificationDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Notifications;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The notification controller.
+    /// </summary>
+    public class NotificationsController : BaseJellyfinApiController
+    {
+        private readonly INotificationManager _notificationManager;
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NotificationsController" /> class.
+        /// </summary>
+        /// <param name="notificationManager">The notification manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        public NotificationsController(INotificationManager notificationManager, IUserManager userManager)
+        {
+            _notificationManager = notificationManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Endpoint for getting a user's notifications.
+        /// </summary>
+        /// <param name="UserID">The UserID.</param>
+        /// <param name="IsRead">An optional filter by IsRead.</param>
+        /// <param name="StartIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
+        /// <param name="Limit">An optional limit on the number of notifications returned.</param>
+        /// <returns>A read-only list of all of the user's notifications.</returns>
+        [HttpGet("{UserID}")]
+        public IReadOnlyList<NotificationDto> GetNotifications(
+            [FromRoute] string UserID,
+            [FromQuery] bool? IsRead,
+            [FromQuery] int? StartIndex,
+            [FromQuery] int? Limit)
+        {
+            return new List<NotificationDto>();
+        }
+
+        /// <summary>
+        /// Endpoint for getting a user's notification summary.
+        /// </summary>
+        /// <param name="UserID">The UserID.</param>
+        /// <returns>Notifications summary for the user.</returns>
+        [HttpGet("{UserId}/Summary")]
+        public NotificationsSummaryDto GetNotificationsSummary(
+            [FromRoute] string UserID)
+        {
+            return new NotificationsSummaryDto();
+        }
+
+        /// <summary>
+        /// Endpoint for getting notification types.
+        /// </summary>
+        /// <returns>All notification types.</returns>
+        [HttpGet("Types")]
+        public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
+        {
+            return _notificationManager.GetNotificationTypes();
+        }
+
+        /// <summary>
+        /// Endpoint for getting notification services.
+        /// </summary>
+        /// <returns>All notification services.</returns>
+        [HttpGet("Services")]
+        public IEnumerable<NameIdPair> GetNotificationServices()
+        {
+            return _notificationManager.GetNotificationServices();
+        }
+
+        /// <summary>
+        /// Endpoint to send a notification to all admins.
+        /// </summary>
+        /// <param name="Name">The name of the notification.</param>
+        /// <param name="Description">The description of the notification.</param>
+        /// <param name="URL">The URL of the notification.</param>
+        /// <param name="Level">The level of the notification.</param>
+        [HttpPost("Admin")]
+        public void CreateAdminNotification(
+            [FromForm] string Name,
+            [FromForm] string Description,
+            [FromForm] string? URL,
+            [FromForm] NotificationLevel Level)
+        {
+            var notification = new NotificationRequest
+            {
+                Name = Name,
+                Description = Description,
+                Url = URL,
+                Level = Level,
+                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
+                Date = DateTime.UtcNow,
+            };
+
+            _notificationManager.SendNotification(notification, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Endpoint to set notifications as read.
+        /// </summary>
+        /// <param name="UserID">The UserID.</param>
+        /// <param name="IDs">The IDs of notifications which should be set as read.</param>
+        [HttpPost("{UserID}/Read")]
+        public void SetRead(
+            [FromRoute] string UserID,
+            [FromForm] List<string> IDs)
+        {
+        }
+
+        /// <summary>
+        /// Endpoint to set notifications as unread.
+        /// </summary>
+        /// <param name="UserID">The UserID.</param>
+        /// <param name="IDs">The IDs of notifications which should be set as unread.</param>
+        [HttpPost("{UserID}/Unread")]
+        public void SetUnread(
+            [FromRoute] string UserID,
+            [FromForm] List<string> IDs)
+        {
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
new file mode 100644
index 0000000000..7ecd2a49db
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -0,0 +1,51 @@
+using System;
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+    /// <summary>
+    /// The notification DTO.
+    /// </summary>
+    public class NotificationDto
+    {
+        /// <summary>
+        /// Gets or sets the notification ID. Defaults to an empty string.
+        /// </summary>
+        public string Id { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the notification's user ID. Defaults to an empty string.
+        /// </summary>
+        public string UserId { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the notification date.
+        /// </summary>
+        public DateTime Date { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the notification has been read.
+        /// </summary>
+        public bool IsRead { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification's name.
+        /// </summary>
+        public string Name { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the notification's description.
+        /// </summary>
+        public string Description { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the notification's URL.
+        /// </summary>
+        public string Url { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the notification level.
+        /// </summary>
+        public NotificationLevel Level { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
new file mode 100644
index 0000000000..c18ab545d3
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+    /// <summary>
+    /// The notification summary DTO.
+    /// </summary>
+    public class NotificationsSummaryDto
+    {
+        /// <summary>
+        /// Gets or sets the number of unread notifications.
+        /// </summary>
+        public int UnreadCount { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum unread notification level.
+        /// </summary>
+        public NotificationLevel MaxUnreadNotificationLevel { get; set; }
+    }
+}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index dd4f9cd238..b3164e068f 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -71,6 +71,7 @@ namespace Jellyfin.Server.Extensions
                 // Clear app parts to avoid other assemblies being picked up
                 .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
                 .AddApplicationPart(typeof(StartupController).Assembly)
+                .AddApplicationPart(typeof(NotificationsController).Assembly)
                 .AddControllersAsServices();
         }
 

From ad1c880751dda93f1226e3846bb6a344ac58d0b6 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 15 Apr 2020 00:34:50 -0600
Subject: [PATCH 002/463] Lowercase parameters

---
 .../Controllers/NotificationsController.cs    | 63 +++++++++----------
 1 file changed, 31 insertions(+), 32 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 6602fca9c7..31747584e1 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,6 +1,5 @@
 #nullable enable
 #pragma warning disable CA1801
-#pragma warning disable SA1313
 
 using System;
 using System.Collections.Generic;
@@ -37,17 +36,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for getting a user's notifications.
         /// </summary>
-        /// <param name="UserID">The UserID.</param>
-        /// <param name="IsRead">An optional filter by IsRead.</param>
-        /// <param name="StartIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
-        /// <param name="Limit">An optional limit on the number of notifications returned.</param>
+        /// <param name="userID">The UserID.</param>
+        /// <param name="isRead">An optional filter by IsRead.</param>
+        /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
+        /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
         public IReadOnlyList<NotificationDto> GetNotifications(
-            [FromRoute] string UserID,
-            [FromQuery] bool? IsRead,
-            [FromQuery] int? StartIndex,
-            [FromQuery] int? Limit)
+            [FromRoute] string userID,
+            [FromQuery] bool? isRead,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit)
         {
             return new List<NotificationDto>();
         }
@@ -55,11 +54,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for getting a user's notification summary.
         /// </summary>
-        /// <param name="UserID">The UserID.</param>
+        /// <param name="userID">The userID.</param>
         /// <returns>Notifications summary for the user.</returns>
-        [HttpGet("{UserId}/Summary")]
+        [HttpGet("{UserID}/Summary")]
         public NotificationsSummaryDto GetNotificationsSummary(
-            [FromRoute] string UserID)
+            [FromRoute] string userID)
         {
             return new NotificationsSummaryDto();
         }
@@ -87,23 +86,23 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint to send a notification to all admins.
         /// </summary>
-        /// <param name="Name">The name of the notification.</param>
-        /// <param name="Description">The description of the notification.</param>
-        /// <param name="URL">The URL of the notification.</param>
-        /// <param name="Level">The level of the notification.</param>
+        /// <param name="name">The name of the notification.</param>
+        /// <param name="description">The description of the notification.</param>
+        /// <param name="url">The URL of the notification.</param>
+        /// <param name="level">The level of the notification.</param>
         [HttpPost("Admin")]
         public void CreateAdminNotification(
-            [FromForm] string Name,
-            [FromForm] string Description,
-            [FromForm] string? URL,
-            [FromForm] NotificationLevel Level)
+            [FromForm] string name,
+            [FromForm] string description,
+            [FromForm] string? url,
+            [FromForm] NotificationLevel level)
         {
             var notification = new NotificationRequest
             {
-                Name = Name,
-                Description = Description,
-                Url = URL,
-                Level = Level,
+                Name = name,
+                Description = description,
+                Url = url,
+                Level = level,
                 UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
                 Date = DateTime.UtcNow,
             };
@@ -114,24 +113,24 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint to set notifications as read.
         /// </summary>
-        /// <param name="UserID">The UserID.</param>
-        /// <param name="IDs">The IDs of notifications which should be set as read.</param>
+        /// <param name="userID">The userID.</param>
+        /// <param name="ids">The IDs of notifications which should be set as read.</param>
         [HttpPost("{UserID}/Read")]
         public void SetRead(
-            [FromRoute] string UserID,
-            [FromForm] List<string> IDs)
+            [FromRoute] string userID,
+            [FromForm] List<string> ids)
         {
         }
 
         /// <summary>
         /// Endpoint to set notifications as unread.
         /// </summary>
-        /// <param name="UserID">The UserID.</param>
-        /// <param name="IDs">The IDs of notifications which should be set as unread.</param>
+        /// <param name="userID">The userID.</param>
+        /// <param name="ids">The IDs of notifications which should be set as unread.</param>
         [HttpPost("{UserID}/Unread")]
         public void SetUnread(
-            [FromRoute] string UserID,
-            [FromForm] List<string> IDs)
+            [FromRoute] string userID,
+            [FromForm] List<string> ids)
         {
         }
     }

From 558b50a094adc82728a52b13862e19bc04783679 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 15 Apr 2020 09:29:29 -0600
Subject: [PATCH 003/463] Remove unnecessary assembly, update casing, enable
 nullable reference types on notification DTOs.

---
 .../Controllers/NotificationsController.cs    | 20 +++++++++----------
 .../NotificationDtos/NotificationDto.cs       | 16 ++++++++-------
 .../NotificationsSummaryDto.cs                |  4 +++-
 .../ApiServiceCollectionExtensions.cs         |  1 -
 4 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 31747584e1..c8a5be89b3 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -36,14 +36,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for getting a user's notifications.
         /// </summary>
-        /// <param name="userID">The UserID.</param>
+        /// <param name="userId">The user's ID.</param>
         /// <param name="isRead">An optional filter by IsRead.</param>
         /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
         public IReadOnlyList<NotificationDto> GetNotifications(
-            [FromRoute] string userID,
+            [FromRoute] string userId,
             [FromQuery] bool? isRead,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit)
@@ -54,11 +54,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for getting a user's notification summary.
         /// </summary>
-        /// <param name="userID">The userID.</param>
+        /// <param name="userId">The user's ID.</param>
         /// <returns>Notifications summary for the user.</returns>
         [HttpGet("{UserID}/Summary")]
         public NotificationsSummaryDto GetNotificationsSummary(
-            [FromRoute] string userID)
+            [FromRoute] string userId)
         {
             return new NotificationsSummaryDto();
         }
@@ -95,14 +95,14 @@ namespace Jellyfin.Api.Controllers
             [FromForm] string name,
             [FromForm] string description,
             [FromForm] string? url,
-            [FromForm] NotificationLevel level)
+            [FromForm] NotificationLevel? level)
         {
             var notification = new NotificationRequest
             {
                 Name = name,
                 Description = description,
                 Url = url,
-                Level = level,
+                Level = level ?? NotificationLevel.Normal,
                 UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
                 Date = DateTime.UtcNow,
             };
@@ -113,11 +113,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint to set notifications as read.
         /// </summary>
-        /// <param name="userID">The userID.</param>
+        /// <param name="userId">The userID.</param>
         /// <param name="ids">The IDs of notifications which should be set as read.</param>
         [HttpPost("{UserID}/Read")]
         public void SetRead(
-            [FromRoute] string userID,
+            [FromRoute] string userId,
             [FromForm] List<string> ids)
         {
         }
@@ -125,11 +125,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint to set notifications as unread.
         /// </summary>
-        /// <param name="userID">The userID.</param>
+        /// <param name="userId">The userID.</param>
         /// <param name="ids">The IDs of notifications which should be set as unread.</param>
         [HttpPost("{UserID}/Unread")]
         public void SetUnread(
-            [FromRoute] string userID,
+            [FromRoute] string userId,
             [FromForm] List<string> ids)
         {
         }
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
index 7ecd2a49db..c849ecd75d 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using MediaBrowser.Model.Notifications;
 
@@ -24,28 +26,28 @@ namespace Jellyfin.Api.Models.NotificationDtos
         public DateTime Date { get; set; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the notification has been read.
+        /// Gets or sets a value indicating whether the notification has been read. Defaults to false.
         /// </summary>
-        public bool IsRead { get; set; }
+        public bool IsRead { get; set; } = false;
 
         /// <summary>
-        /// Gets or sets the notification's name.
+        /// Gets or sets the notification's name. Defaults to an empty string.
         /// </summary>
         public string Name { get; set; } = string.Empty;
 
         /// <summary>
-        /// Gets or sets the notification's description.
+        /// Gets or sets the notification's description. Defaults to an empty string.
         /// </summary>
         public string Description { get; set; } = string.Empty;
 
         /// <summary>
-        /// Gets or sets the notification's URL.
+        /// Gets or sets the notification's URL. Defaults to null.
         /// </summary>
-        public string Url { get; set; } = string.Empty;
+        public string? Url { get; set; }
 
         /// <summary>
         /// Gets or sets the notification level.
         /// </summary>
-        public NotificationLevel Level { get; set; }
+        public NotificationLevel? Level { get; set; }
     }
 }
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
index c18ab545d3..b3746ee2da 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 using MediaBrowser.Model.Notifications;
 
 namespace Jellyfin.Api.Models.NotificationDtos
@@ -15,6 +17,6 @@ namespace Jellyfin.Api.Models.NotificationDtos
         /// <summary>
         /// Gets or sets the maximum unread notification level.
         /// </summary>
-        public NotificationLevel MaxUnreadNotificationLevel { get; set; }
+        public NotificationLevel? MaxUnreadNotificationLevel { get; set; }
     }
 }
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index b3164e068f..dd4f9cd238 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -71,7 +71,6 @@ namespace Jellyfin.Server.Extensions
                 // Clear app parts to avoid other assemblies being picked up
                 .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
                 .AddApplicationPart(typeof(StartupController).Assembly)
-                .AddApplicationPart(typeof(NotificationsController).Assembly)
                 .AddControllersAsServices();
         }
 

From 8a7e4cd639be24eb58385dc7b36b466c3d6aed92 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 10:51:51 -0600
Subject: [PATCH 004/463] add redoc

---
 Jellyfin.Api/Jellyfin.Api.csproj                 |  3 ++-
 .../ApiApplicationBuilderExtensions.cs           | 16 ++++++++++------
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 8f23ef9d03..cbb1d3007f 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -10,7 +10,8 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.3" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.3.2" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.2" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index db06eb4552..2ab9b0ba5e 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -14,14 +14,18 @@ namespace Jellyfin.Server.Extensions
         /// <returns>The updated application builder.</returns>
         public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder)
         {
-            applicationBuilder.UseSwagger();
-
             // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
             // specifying the Swagger JSON endpoint.
-            return applicationBuilder.UseSwaggerUI(c =>
-            {
-                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1");
-            });
+            const string specEndpoint = "/swagger/v1/swagger.json";
+            return applicationBuilder.UseSwagger()
+                .UseSwaggerUI(c =>
+                {
+                    c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1");
+                })
+                .UseReDoc(c =>
+                {
+                    c.SpecUrl(specEndpoint);
+                });
         }
     }
 }

From e72a543570b59df61f48cb9a4049ab3dc9675250 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 11:24:03 -0600
Subject: [PATCH 005/463] Add Redoc, move docs to api-docs/

---
 Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 2ab9b0ba5e..766243f201 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -21,10 +21,12 @@ namespace Jellyfin.Server.Extensions
                 .UseSwaggerUI(c =>
                 {
                     c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1");
+                    c.RoutePrefix = "api-docs/swagger";
                 })
                 .UseReDoc(c =>
                 {
                     c.SpecUrl(specEndpoint);
+                    c.RoutePrefix = "api-docs/redoc";
                 });
         }
     }

From 5da88fac4d0681126bdee635d59237d8d7fcebeb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 11:24:32 -0600
Subject: [PATCH 006/463] Enable string enum converter

---
 .../ApiServiceCollectionExtensions.cs         | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 71ef9a69a2..a4f078b5b3 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json.Serialization;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
@@ -75,6 +80,9 @@ namespace Jellyfin.Server.Extensions
                 {
                     // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
                     options.JsonSerializerOptions.PropertyNamingPolicy = null;
+
+                    // Accept string enums
+                    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
                 })
                 .AddControllersAsServices();
         }
@@ -89,6 +97,17 @@ namespace Jellyfin.Server.Extensions
             return serviceCollection.AddSwaggerGen(c =>
             {
                 c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+
+                // Add all xml doc files to swagger generator.
+                var xmlFiles = Directory.GetFiles(
+                    AppContext.BaseDirectory,
+                    "*.xml",
+                    SearchOption.TopDirectoryOnly);
+
+                foreach (var xmlFile in xmlFiles)
+                {
+                    c.IncludeXmlComments(xmlFile);
+                }
             });
         }
     }

From 72745f47225a5b1071660acc4dcde618d938eaa0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 11:28:56 -0600
Subject: [PATCH 007/463] fix formatting

---
 Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 766243f201..43c49307d4 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -17,7 +17,8 @@ namespace Jellyfin.Server.Extensions
             // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
             // specifying the Swagger JSON endpoint.
             const string specEndpoint = "/swagger/v1/swagger.json";
-            return applicationBuilder.UseSwagger()
+            return applicationBuilder
+                .UseSwagger()
                 .UseSwaggerUI(c =>
                 {
                     c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1");

From 86d68e23e7af367152edc36977a9a39431bd2641 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 12:06:18 -0600
Subject: [PATCH 008/463] Add DisplayPreferencesController

---
 .../Controllers/DisplayPreferencesController.cs       | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/DisplayPreferencesController.cs

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
new file mode 100644
index 0000000000..537a940460
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Display Preferences Controller.
+    /// </summary>
+    public class DisplayPreferencesController : BaseJellyfinApiController
+    {
+    }
+}

From a282fbe9668263481b850b29b3fb8064d4d7ee9f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 12:26:38 -0600
Subject: [PATCH 009/463] Move DisplayPreferences to Jellyfin.Api

---
 .../DisplayPreferencesController.cs           |  92 ++++++++++++++++
 MediaBrowser.Api/DisplayPreferencesService.cs | 101 ------------------
 2 files changed, 92 insertions(+), 101 deletions(-)
 delete mode 100644 MediaBrowser.Api/DisplayPreferencesService.cs

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 537a940460..6182c3507b 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,4 +1,11 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -7,5 +14,90 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     public class DisplayPreferencesController : BaseJellyfinApiController
     {
+        private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
+        /// </summary>
+        /// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param>
+        public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository)
+        {
+            _displayPreferencesRepository = displayPreferencesRepository;
+        }
+
+        /// <summary>
+        /// Get Display Preferences
+        /// </summary>
+        /// <param name="displayPreferencesId">Display preferences id.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="client">Client.</param>
+        /// <returns>Display Preferences.</returns>
+        [HttpGet("{DisplayPreferencesId")]
+        [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+        public IActionResult GetDisplayPreferences(
+            [FromRoute] string displayPreferencesId,
+            [FromQuery] [Required] string userId,
+            [FromQuery] [Required] string client
+        )
+        {
+            try
+            {
+                var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
+                if (result == null)
+                {
+                    return NotFound();
+                }
+
+                // TODO ToOptimizedResult
+                return Ok(result);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Update Display Preferences
+        /// </summary>
+        /// <param name="displayPreferencesId">Display preferences id.</param>
+        /// <param name="userId">User Id.</param>
+        /// <param name="client">Client.</param>
+        /// <param name="displayPreferences">New Display Preferences object.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("{DisplayPreferencesId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult UpdateDisplayPreferences(
+            [FromRoute] string displayPreferencesId,
+            [FromQuery, BindRequired] string userId,
+            [FromQuery, BindRequired] string client,
+            [FromBody, BindRequired] DisplayPreferences displayPreferences)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    return BadRequest(ModelState);
+                }
+
+                displayPreferences.Id = displayPreferencesId;
+                _displayPreferencesRepository.SaveDisplayPreferences(
+                    displayPreferences,
+                    userId,
+                    client,
+                    CancellationToken.None);
+
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
     }
 }
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
deleted file mode 100644
index 62c4ff43f2..0000000000
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Threading;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class UpdateDisplayPreferences
-    /// </summary>
-    [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
-    public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string DisplayPreferencesId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferences>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Client { get; set; }
-    }
-
-    /// <summary>
-    /// Class DisplayPreferencesService
-    /// </summary>
-    [Authenticated]
-    public class DisplayPreferencesService : BaseApiService
-    {
-        /// <summary>
-        /// The _display preferences manager
-        /// </summary>
-        private readonly IDisplayPreferencesRepository _displayPreferencesManager;
-        /// <summary>
-        /// The _json serializer
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
-        /// </summary>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="displayPreferencesManager">The display preferences manager.</param>
-        public DisplayPreferencesService(
-            ILogger<DisplayPreferencesService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IDisplayPreferencesRepository displayPreferencesManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _jsonSerializer = jsonSerializer;
-            _displayPreferencesManager = displayPreferencesManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Get(GetDisplayPreferences request)
-        {
-            var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateDisplayPreferences request)
-        {
-            // Serialize to json and then back so that the core doesn't see the request dto type
-            var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
-
-            _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
-        }
-    }
-}

From c31b9f5169ae62787fa356ccecc2f1fc6896d04b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 12:30:10 -0600
Subject: [PATCH 010/463] Fix build & runtime errors

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 6182c3507b..a3bcafaea5 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -26,21 +26,20 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Get Display Preferences
+        /// Get Display Preferences.
         /// </summary>
         /// <param name="displayPreferencesId">Display preferences id.</param>
         /// <param name="userId">User id.</param>
         /// <param name="client">Client.</param>
         /// <returns>Display Preferences.</returns>
-        [HttpGet("{DisplayPreferencesId")]
+        [HttpGet("{DisplayPreferencesId}")]
         [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status500InternalServerError)]
         public IActionResult GetDisplayPreferences(
             [FromRoute] string displayPreferencesId,
             [FromQuery] [Required] string userId,
-            [FromQuery] [Required] string client
-        )
+            [FromQuery] [Required] string client)
         {
             try
             {
@@ -60,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Update Display Preferences
+        /// Update Display Preferences.
         /// </summary>
         /// <param name="displayPreferencesId">Display preferences id.</param>
         /// <param name="userId">User Id.</param>

From 60607ab60c3051815179859adfd2a7182f9ceb9a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 12:34:34 -0600
Subject: [PATCH 011/463] Fix saving DisplayPreferences

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index a3bcafaea5..2c4072b39f 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -84,7 +84,11 @@ namespace Jellyfin.Api.Controllers
                     return BadRequest(ModelState);
                 }
 
-                displayPreferences.Id = displayPreferencesId;
+                if (displayPreferencesId == null)
+                {
+                    // do nothing.
+                }
+
                 _displayPreferencesRepository.SaveDisplayPreferences(
                     displayPreferences,
                     userId,

From e6b873f2aeadd01ed4638148be857ddf45a33576 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 12:56:16 -0600
Subject: [PATCH 012/463] Fix missing attributes

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 2c4072b39f..0fbdcb6b80 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,6 +1,9 @@
+#nullable enable
+
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.Threading;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Http;
@@ -12,6 +15,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Display Preferences Controller.
     /// </summary>
+    [Authenticated]
     public class DisplayPreferencesController : BaseJellyfinApiController
     {
         private readonly IDisplayPreferencesRepository _displayPreferencesRepository;

From 7c8188194b5bf9b74413f25d471a212f1677f7ed Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Sun, 19 Apr 2020 13:19:15 -0600
Subject: [PATCH 013/463] Address PR comments, and revert changes that changed
 the API schema

---
 .../Controllers/NotificationsController.cs    | 20 +++++++++---------
 .../NotificationDtos/NotificationDto.cs       |  6 +++---
 .../NotificationDtos/NotificationResultDto.cs | 21 +++++++++++++++++++
 3 files changed, 34 insertions(+), 13 deletions(-)
 create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index c8a5be89b3..d9a5c5e316 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -42,13 +42,13 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
-        public IReadOnlyList<NotificationDto> GetNotifications(
+        public NotificationResultDto GetNotifications(
             [FromRoute] string userId,
             [FromQuery] bool? isRead,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit)
         {
-            return new List<NotificationDto>();
+            return new NotificationResultDto();
         }
 
         /// <summary>
@@ -92,10 +92,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="level">The level of the notification.</param>
         [HttpPost("Admin")]
         public void CreateAdminNotification(
-            [FromForm] string name,
-            [FromForm] string description,
-            [FromForm] string? url,
-            [FromForm] NotificationLevel? level)
+            [FromQuery] string name,
+            [FromQuery] string description,
+            [FromQuery] string? url,
+            [FromQuery] NotificationLevel? level)
         {
             var notification = new NotificationRequest
             {
@@ -114,11 +114,11 @@ namespace Jellyfin.Api.Controllers
         /// Endpoint to set notifications as read.
         /// </summary>
         /// <param name="userId">The userID.</param>
-        /// <param name="ids">The IDs of notifications which should be set as read.</param>
+        /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
         [HttpPost("{UserID}/Read")]
         public void SetRead(
             [FromRoute] string userId,
-            [FromForm] List<string> ids)
+            [FromQuery] string ids)
         {
         }
 
@@ -126,11 +126,11 @@ namespace Jellyfin.Api.Controllers
         /// Endpoint to set notifications as unread.
         /// </summary>
         /// <param name="userId">The userID.</param>
-        /// <param name="ids">The IDs of notifications which should be set as unread.</param>
+        /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
         [HttpPost("{UserID}/Unread")]
         public void SetUnread(
             [FromRoute] string userId,
-            [FromForm] List<string> ids)
+            [FromQuery] string ids)
         {
         }
     }
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
index c849ecd75d..502b22623b 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -41,13 +41,13 @@ namespace Jellyfin.Api.Models.NotificationDtos
         public string Description { get; set; } = string.Empty;
 
         /// <summary>
-        /// Gets or sets the notification's URL. Defaults to null.
+        /// Gets or sets the notification's URL. Defaults to an empty string.
         /// </summary>
-        public string? Url { get; set; }
+        public string Url { get; set; } = string.Empty;
 
         /// <summary>
         /// Gets or sets the notification level.
         /// </summary>
-        public NotificationLevel? Level { get; set; }
+        public NotificationLevel Level { get; set; }
     }
 }
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
new file mode 100644
index 0000000000..64e92bd83a
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+    /// <summary>
+    /// A list of notifications with the total record count for pagination.
+    /// </summary>
+    public class NotificationResultDto
+    {
+        /// <summary>
+        /// Gets or sets the current page of notifications.
+        /// </summary>
+        public IReadOnlyList<NotificationDto> Notifications { get; set; } = Array.Empty<NotificationDto>();
+
+        /// <summary>
+        /// Gets or sets the total number of notifications.
+        /// </summary>
+        public int TotalRecordCount { get; set; }
+    }
+}

From 5d9c40ec72d31957cec48e141ca5ce4f9141b413 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 16:26:20 -0600
Subject: [PATCH 014/463] move scheduled tasks to Jellyfin.Api

---
 .../Controllers/ScheduledTasksController.cs   | 207 ++++++++++++++++++
 .../Converters/LongToStringConverter.cs       |  56 +++++
 .../ApiServiceCollectionExtensions.cs         |   2 +
 3 files changed, 265 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/ScheduledTasksController.cs
 create mode 100644 Jellyfin.Server/Converters/LongToStringConverter.cs

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
new file mode 100644
index 0000000000..bb07af3979
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -0,0 +1,207 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Scheduled Tasks Controller.
+    /// </summary>
+    public class ScheduledTasksController : BaseJellyfinApiController
+    {
+        private readonly ITaskManager _taskManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
+        /// </summary>
+        /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
+        public ScheduledTasksController(ITaskManager taskManager)
+        {
+            _taskManager = taskManager;
+        }
+
+        /// <summary>
+        /// Get tasks.
+        /// </summary>
+        /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
+        /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
+        /// <returns>Task list.</returns>
+        [HttpGet]
+        [ProducesResponseType(typeof(TaskInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetTasks(
+            [FromQuery] bool? isHidden = false,
+            [FromQuery] bool? isEnabled = false)
+        {
+            try
+            {
+                IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
+
+                if (isHidden.HasValue)
+                {
+                    var hiddenValue = isHidden.Value;
+                    tasks = tasks.Where(o =>
+                    {
+                        var itemIsHidden = false;
+                        if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
+                        {
+                            itemIsHidden = configurableScheduledTask.IsHidden;
+                        }
+
+                        return itemIsHidden == hiddenValue;
+                    });
+                }
+
+                if (isEnabled.HasValue)
+                {
+                    var enabledValue = isEnabled.Value;
+                    tasks = tasks.Where(o =>
+                    {
+                        var itemIsEnabled = false;
+                        if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
+                        {
+                            itemIsEnabled = configurableScheduledTask.IsEnabled;
+                        }
+
+                        return itemIsEnabled == enabledValue;
+                    });
+                }
+
+                var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo);
+
+                // TODO ToOptimizedResult
+                return Ok(taskInfos);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Get task by id.
+        /// </summary>
+        /// <param name="taskId">Task Id.</param>
+        /// <returns>Task Info.</returns>
+        [HttpGet("{TaskID}")]
+        [ProducesResponseType(typeof(TaskInfo), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetTask([FromRoute] string taskId)
+        {
+            try
+            {
+                var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
+                    string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
+
+                if (task == null)
+                {
+                    return NotFound();
+                }
+
+                var result = ScheduledTaskHelpers.GetTaskInfo(task);
+                return Ok(result);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Start specified task.
+        /// </summary>
+        /// <param name="taskId">Task Id.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Running/{TaskID}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult StartTask([FromRoute] string taskId)
+        {
+            try
+            {
+                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+
+                if (task == null)
+                {
+                    return NotFound();
+                }
+
+                _taskManager.Execute(task, new TaskOptions());
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Stop specified task.
+        /// </summary>
+        /// <param name="taskId">Task Id.</param>
+        /// <returns>Status.</returns>
+        [HttpDelete("Running/{TaskID}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult StopTask([FromRoute] string taskId)
+        {
+            try
+            {
+                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+
+                if (task == null)
+                {
+                    return NotFound();
+                }
+
+                _taskManager.Cancel(task);
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Update specified task triggers.
+        /// </summary>
+        /// <param name="taskId">Task Id.</param>
+        /// <param name="triggerInfos">Triggers.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("{TaskID}/Triggers")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult UpdateTask([FromRoute] string taskId, [FromBody] TaskTriggerInfo[] triggerInfos)
+        {
+            try
+            {
+                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+                if (task == null)
+                {
+                    return NotFound();
+                }
+
+                task.Triggers = triggerInfos;
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Server/Converters/LongToStringConverter.cs b/Jellyfin.Server/Converters/LongToStringConverter.cs
new file mode 100644
index 0000000000..ad66b7b0c3
--- /dev/null
+++ b/Jellyfin.Server/Converters/LongToStringConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Server.Converters
+{
+    /// <summary>
+    /// Long to String JSON converter.
+    /// Javascript does not support 64-bit integers.
+    /// </summary>
+    public class LongToStringConverter : JsonConverter<long>
+    {
+        /// <summary>
+        /// Read JSON string as Long.
+        /// </summary>
+        /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
+        /// <param name="type">Type.</param>
+        /// <param name="options">Options.</param>
+        /// <returns>Parsed value.</returns>
+        public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+        {
+            if (reader.TokenType == JsonTokenType.String)
+            {
+                // try to parse number directly from bytes
+                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+                if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed)
+                {
+                    return number;
+                }
+
+                // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
+                if (long.TryParse(reader.GetString(), out number))
+                {
+                    return number;
+                }
+            }
+
+            // fallback to default handling
+            return reader.GetInt64();
+        }
+
+        /// <summary>
+        /// Write long to JSON string.
+        /// </summary>
+        /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
+        /// <param name="value">Value to write.</param>
+        /// <param name="options">Options.</param>
+        public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
+        {
+            writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo));
+        }
+    }
+}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 71ef9a69a2..afd42ac5ac 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -4,6 +4,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Converters;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -75,6 +76,7 @@ namespace Jellyfin.Server.Extensions
                 {
                     // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
                     options.JsonSerializerOptions.PropertyNamingPolicy = null;
+                    options.JsonSerializerOptions.Converters.Add(new LongToStringConverter());
                 })
                 .AddControllersAsServices();
         }

From d8fc4f91dbcc38df0e13e51a3631e87f783361de Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 16:29:29 -0600
Subject: [PATCH 015/463] burn ToOptimizedResult

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index bb07af3979..f90b449673 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -75,7 +75,6 @@ namespace Jellyfin.Api.Controllers
 
                 var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo);
 
-                // TODO ToOptimizedResult
                 return Ok(taskInfos);
             }
             catch (Exception e)

From 4a960892c20676ce6400f4cae1c85e8ce4d4a841 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 16:31:09 -0600
Subject: [PATCH 016/463] Add Authorize and BindRequired

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index f90b449673..157e985197 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -3,6 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -13,6 +14,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Scheduled Tasks Controller.
     /// </summary>
+    [Authenticated]
     public class ScheduledTasksController : BaseJellyfinApiController
     {
         private readonly ITaskManager _taskManager;
@@ -183,7 +185,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult UpdateTask([FromRoute] string taskId, [FromBody] TaskTriggerInfo[] triggerInfos)
+        public IActionResult UpdateTask([FromRoute] string taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
         {
             try
             {

From a96db5f48e57a192369b220422517171c06411b6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 16:32:03 -0600
Subject: [PATCH 017/463] Remove old scheduled tasks service

---
 .../ScheduledTasks/ScheduledTaskService.cs    | 234 ------------------
 1 file changed, 234 deletions(-)
 delete mode 100644 MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs

diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
deleted file mode 100644
index e08a8482e0..0000000000
--- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.ScheduledTasks
-{
-    /// <summary>
-    /// Class GetScheduledTask
-    /// </summary>
-    [Route("/ScheduledTasks/{Id}", "GET", Summary = "Gets a scheduled task, by Id")]
-    public class GetScheduledTask : IReturn<TaskInfo>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetScheduledTasks
-    /// </summary>
-    [Route("/ScheduledTasks", "GET", Summary = "Gets scheduled tasks")]
-    public class GetScheduledTasks : IReturn<TaskInfo[]>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional filter tasks that are hidden, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-
-        [ApiMember(Name = "IsEnabled", Description = "Optional filter tasks that are enabled, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsEnabled { get; set; }
-    }
-
-    /// <summary>
-    /// Class StartScheduledTask
-    /// </summary>
-    [Route("/ScheduledTasks/Running/{Id}", "POST", Summary = "Starts a scheduled task")]
-    public class StartScheduledTask : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class StopScheduledTask
-    /// </summary>
-    [Route("/ScheduledTasks/Running/{Id}", "DELETE", Summary = "Stops a scheduled task")]
-    public class StopScheduledTask : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateScheduledTaskTriggers
-    /// </summary>
-    [Route("/ScheduledTasks/{Id}/Triggers", "POST", Summary = "Updates the triggers for a scheduled task")]
-    public class UpdateScheduledTaskTriggers : List<TaskTriggerInfo>, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the task id.
-        /// </summary>
-        /// <value>The task id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class ScheduledTasksService
-    /// </summary>
-    [Authenticated(Roles = "Admin")]
-    public class ScheduledTaskService : BaseApiService
-    {
-        /// <summary>
-        /// The task manager.
-        /// </summary>
-        private readonly ITaskManager _taskManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScheduledTaskService" /> class.
-        /// </summary>
-        /// <param name="taskManager">The task manager.</param>
-        /// <exception cref="ArgumentNullException">taskManager</exception>
-        public ScheduledTaskService(
-            ILogger<ScheduledTaskService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ITaskManager taskManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _taskManager = taskManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{TaskInfo}.</returns>
-        public object Get(GetScheduledTasks request)
-        {
-            IEnumerable<IScheduledTaskWorker> result = _taskManager.ScheduledTasks
-                .OrderBy(i => i.Name);
-
-            if (request.IsHidden.HasValue)
-            {
-                var val = request.IsHidden.Value;
-
-                result = result.Where(i =>
-                {
-                    var isHidden = false;
-
-                    if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
-                    {
-                        isHidden = configurableTask.IsHidden;
-                    }
-
-                    return isHidden == val;
-                });
-            }
-
-            if (request.IsEnabled.HasValue)
-            {
-                var val = request.IsEnabled.Value;
-
-                result = result.Where(i =>
-                {
-                    var isEnabled = true;
-
-                    if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
-                    {
-                        isEnabled = configurableTask.IsEnabled;
-                    }
-
-                    return isEnabled == val;
-                });
-            }
-
-            var infos = result
-                .Select(ScheduledTaskHelpers.GetTaskInfo)
-                .ToArray();
-
-            return ToOptimizedResult(infos);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{TaskInfo}.</returns>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public object Get(GetScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            var result = ScheduledTaskHelpers.GetTaskInfo(task);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Post(StartScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            _taskManager.Execute(task, new TaskOptions());
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Delete(StopScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            _taskManager.Cancel(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Post(UpdateScheduledTaskTriggers request)
-        {
-            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
-            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var id = GetPathValue(1).ToString();
-
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.Ordinal));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            task.Triggers = request.ToArray();
-        }
-    }
-}

From c5d709f77ed2158bf68b8cc81238067d4525518f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 16:35:31 -0600
Subject: [PATCH 018/463] remove todo

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 0fbdcb6b80..0554091b45 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -53,7 +53,6 @@ namespace Jellyfin.Api.Controllers
                     return NotFound();
                 }
 
-                // TODO ToOptimizedResult
                 return Ok(result);
             }
             catch (Exception e)

From a41d5fcea4ee082bb49ddac34a1606204e12e8e8 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 17:36:05 -0600
Subject: [PATCH 019/463] Move AttachmentsService to AttachmentsController

---
 .../Controllers/AttachmentsController.cs      | 86 +++++++++++++++++++
 .../Attachments/AttachmentService.cs          | 63 --------------
 MediaBrowser.Api/MediaBrowser.Api.csproj      |  4 +
 3 files changed, 90 insertions(+), 63 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs
 delete mode 100644 MediaBrowser.Api/Attachments/AttachmentService.cs

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
new file mode 100644
index 0000000000..5d48a79b9b
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Attachments controller.
+    /// </summary>
+    [Route("Videos")]
+    [Authenticated]
+    public class AttachmentsController : Controller
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IAttachmentExtractor _attachmentExtractor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AttachmentsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+        public AttachmentsController(
+            ILibraryManager libraryManager,
+            IAttachmentExtractor attachmentExtractor)
+        {
+            _libraryManager = libraryManager;
+            _attachmentExtractor = attachmentExtractor;
+        }
+
+        /// <summary>
+        /// Get video attachment.
+        /// </summary>
+        /// <param name="videoId">Video ID.</param>
+        /// <param name="mediaSourceId">Media Source ID.</param>
+        /// <param name="index">Attachment Index.</param>
+        /// <returns>Attachment.</returns>
+        [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public async Task<IActionResult> GetAttachment(
+            [FromRoute] Guid videoId,
+            [FromRoute] string mediaSourceId,
+            [FromRoute] int index)
+        {
+            try
+            {
+                var item = _libraryManager.GetItemById(videoId);
+                if (item == null)
+                {
+                    return NotFound();
+                }
+
+                var (attachment, stream) = await _attachmentExtractor.GetAttachment(
+                        item,
+                        mediaSourceId,
+                        index,
+                        CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                var contentType = "application/octet-stream";
+                if (string.IsNullOrWhiteSpace(attachment.MimeType))
+                {
+                    contentType = attachment.MimeType;
+                }
+
+                return new FileStreamResult(stream, contentType);
+            }
+            catch (ResourceNotFoundException e)
+            {
+                return StatusCode(StatusCodes.Status404NotFound, e.Message);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs
deleted file mode 100644
index 1632ca1b06..0000000000
--- a/MediaBrowser.Api/Attachments/AttachmentService.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Attachments
-{
-    [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")]
-    public class GetAttachment
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-    }
-
-    public class AttachmentService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IAttachmentExtractor _attachmentExtractor;
-
-        public AttachmentService(
-            ILogger<AttachmentService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            IAttachmentExtractor attachmentExtractor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _attachmentExtractor = attachmentExtractor;
-        }
-
-        public async Task<object> Get(GetAttachment request)
-        {
-            var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false);
-            var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType;
-
-            return ResultFactory.GetResult(Request, attachmentStream, mime);
-        }
-
-        private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return _attachmentExtractor.GetAttachment(item,
-                request.MediaSourceId,
-                request.Index,
-                CancellationToken.None);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index 0d62cf8c59..5ca74d4238 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -9,6 +9,10 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="Attachments" />
+  </ItemGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

From 1fc682541050e074227736f0c8556d53f98228a1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 17:37:15 -0600
Subject: [PATCH 020/463] nullable

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index 5d48a79b9b..f4c1a761fb 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Threading;
 using System.Threading.Tasks;

From ad67081840ec61085673634795d0b6363f649dbf Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 18:04:36 -0600
Subject: [PATCH 021/463] add camelCase formatter

---
 .../CamelCaseJsonProfileFormatter.cs          | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)
 create mode 100644 Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs

diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
new file mode 100644
index 0000000000..433a3197d3
--- /dev/null
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -0,0 +1,21 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Server.Formatters
+{
+    /// <summary>
+    /// Camel Case Json Profile Formatter.
+    /// </summary>
+    public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
+        /// </summary>
+        public CamelCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
+        {
+            SupportedMediaTypes.Clear();
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
+        }
+    }
+}

From c89dc8921ffb0ce11031e9cfb096b525d94e21b3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 18:10:59 -0600
Subject: [PATCH 022/463] Fix PascalCase

---
 .../ApiServiceCollectionExtensions.cs         |  3 +++
 .../PascalCaseJsonProfileFormatter.cs         | 23 +++++++++++++++++++
 2 files changed, 26 insertions(+)
 create mode 100644 Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 71ef9a69a2..00688074f0 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -4,6 +4,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Formatters;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -66,6 +67,8 @@ namespace Jellyfin.Server.Extensions
             return serviceCollection.AddMvc(opts =>
                 {
                     opts.UseGeneralRoutePrefix(baseUrl);
+                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
+                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
new file mode 100644
index 0000000000..2ed006a336
--- /dev/null
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -0,0 +1,23 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Server.Formatters
+{
+    /// <summary>
+    /// Pascal Case Json Profile Formatter.
+    /// </summary>
+    public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
+        /// </summary>
+        public PascalCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = null })
+        {
+            SupportedMediaTypes.Clear();
+            // Add application/json for default formatter
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\""));
+        }
+    }
+}

From 21b54b4ad8477d654e4f79e9805701c9737346a6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 19:33:55 -0600
Subject: [PATCH 023/463] Move DeviceService to DevicesController

---
 Jellyfin.Api/Controllers/DevicesController.cs | 260 ++++++++++++++++++
 MediaBrowser.Api/Devices/DeviceService.cs     | 168 -----------
 2 files changed, 260 insertions(+), 168 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/DevicesController.cs
 delete mode 100644 MediaBrowser.Api/Devices/DeviceService.cs

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
new file mode 100644
index 0000000000..7407c44878
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -0,0 +1,260 @@
+#nullable enable
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Devices;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Devices Controller.
+    /// </summary>
+    [Authenticated]
+    public class DevicesController : BaseJellyfinApiController
+    {
+        private readonly IDeviceManager _deviceManager;
+        private readonly IAuthenticationRepository _authenticationRepository;
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DevicesController"/> class.
+        /// </summary>
+        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
+        /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+        public DevicesController(
+            IDeviceManager deviceManager,
+            IAuthenticationRepository authenticationRepository,
+            ISessionManager sessionManager)
+        {
+            _deviceManager = deviceManager;
+            _authenticationRepository = authenticationRepository;
+            _sessionManager = sessionManager;
+        }
+
+        /// <summary>
+        /// Get Devices.
+        /// </summary>
+        /// <param name="supportsSync">/// Gets or sets a value indicating whether [supports synchronize].</param>
+        /// <param name="userId">/// Gets or sets the user identifier.</param>
+        /// <returns>Device Infos.</returns>
+        [HttpGet]
+        [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        {
+            try
+            {
+                var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
+                var devices = _deviceManager.GetDevices(deviceQuery);
+                return Ok(devices);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Get info for a device.
+        /// </summary>
+        /// <param name="id">Device Id.</param>
+        /// <returns>Device Info.</returns>
+        [HttpGet("Info")]
+        [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id)
+        {
+            try
+            {
+                var deviceInfo = _deviceManager.GetDevice(id);
+                if (deviceInfo == null)
+                {
+                    return NotFound();
+                }
+
+                return Ok(deviceInfo);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Get options for a device.
+        /// </summary>
+        /// <param name="id">Device Id.</param>
+        /// <returns>Device Info.</returns>
+        [HttpGet("Options")]
+        [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id)
+        {
+            try
+            {
+                var deviceInfo = _deviceManager.GetDeviceOptions(id);
+                if (deviceInfo == null)
+                {
+                    return NotFound();
+                }
+
+                return Ok(deviceInfo);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Update device options.
+        /// </summary>
+        /// <param name="id">Device Id.</param>
+        /// <param name="deviceOptions">Device Options.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Options")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult UpdateDeviceOptions(
+            [FromQuery, BindRequired] string id,
+            [FromBody, BindRequired] DeviceOptions deviceOptions)
+        {
+            try
+            {
+                var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
+                if (existingDeviceOptions == null)
+                {
+                    return NotFound();
+                }
+
+                _deviceManager.UpdateDeviceOptions(id, deviceOptions);
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Deletes a device.
+        /// </summary>
+        /// <param name="id">Device Id.</param>
+        /// <returns>Status.</returns>
+        [HttpDelete]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult DeleteDevice([FromQuery, BindRequired] string id)
+        {
+            try
+            {
+                var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
+
+                foreach (var session in sessions)
+                {
+                    _sessionManager.Logout(session);
+                }
+
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Gets camera upload history for a device.
+        /// </summary>
+        /// <param name="id">Device Id.</param>
+        /// <returns>Content Upload History.</returns>
+        [HttpGet("CameraUploads")]
+        [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetCameraUploads([FromQuery, BindRequired] string id)
+        {
+            try
+            {
+                var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
+                return Ok(uploadHistory);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Uploads content.
+        /// </summary>
+        /// <param name="deviceId">Device Id.</param>
+        /// <param name="album">Album.</param>
+        /// <param name="name">Name.</param>
+        /// <param name="id">Id.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("CameraUploads")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status400BadRequest)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public async Task<IActionResult> PostCameraUploadAsync(
+            [FromQuery, BindRequired] string deviceId,
+            [FromQuery, BindRequired] string album,
+            [FromQuery, BindRequired] string name,
+            [FromQuery, BindRequired] string id)
+        {
+            try
+            {
+                Stream fileStream;
+                string contentType;
+
+                if (Request.HasFormContentType)
+                {
+                    if (Request.Form.Files.Any())
+                    {
+                        fileStream = Request.Form.Files[0].OpenReadStream();
+                        contentType = Request.Form.Files[0].ContentType;
+                    }
+                    else
+                    {
+                        return BadRequest();
+                    }
+                }
+                else
+                {
+                    fileStream = Request.Body;
+                    contentType = Request.ContentType;
+                }
+
+                await _deviceManager.AcceptCameraUpload(
+                    deviceId,
+                    fileStream,
+                    new LocalFileInfo
+                    {
+                        MimeType = contentType,
+                        Album = album,
+                        Name = name,
+                        Id = id
+                    }).ConfigureAwait(false);
+
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs
deleted file mode 100644
index 7004a2559e..0000000000
--- a/MediaBrowser.Api/Devices/DeviceService.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Devices
-{
-    [Route("/Devices", "GET", Summary = "Gets all devices")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDevices : DeviceQuery, IReturn<QueryResult<DeviceInfo>>
-    {
-    }
-
-    [Route("/Devices/Info", "GET", Summary = "Gets info for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceInfo : IReturn<DeviceInfo>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/Options", "GET", Summary = "Gets options for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceOptions : IReturn<DeviceOptions>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices", "DELETE", Summary = "Deletes a device")]
-    public class DeleteDevice
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/CameraUploads", "GET", Summary = "Gets camera upload history for a device")]
-    [Authenticated]
-    public class GetCameraUploads : IReturn<ContentUploadHistory>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string DeviceId { get; set; }
-    }
-
-    [Route("/Devices/CameraUploads", "POST", Summary = "Uploads content")]
-    [Authenticated]
-    public class PostCameraUpload : IRequiresRequestStream, IReturnVoid
-    {
-        [ApiMember(Name = "DeviceId", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string DeviceId { get; set; }
-
-        [ApiMember(Name = "Album", Description = "Album", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Album { get; set; }
-
-        [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Devices/Options", "POST", Summary = "Updates device options")]
-    [Authenticated(Roles = "Admin")]
-    public class PostDeviceOptions : DeviceOptions, IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    public class DeviceService : BaseApiService
-    {
-        private readonly IDeviceManager _deviceManager;
-        private readonly IAuthenticationRepository _authRepo;
-        private readonly ISessionManager _sessionManager;
-
-        public DeviceService(
-            ILogger<DeviceService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDeviceManager deviceManager,
-            IAuthenticationRepository authRepo,
-            ISessionManager sessionManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _deviceManager = deviceManager;
-            _authRepo = authRepo;
-            _sessionManager = sessionManager;
-        }
-
-        public void Post(PostDeviceOptions request)
-        {
-            _deviceManager.UpdateDeviceOptions(request.Id, request);
-        }
-
-        public object Get(GetDevices request)
-        {
-            return ToOptimizedResult(_deviceManager.GetDevices(request));
-        }
-
-        public object Get(GetDeviceInfo request)
-        {
-            return _deviceManager.GetDevice(request.Id);
-        }
-
-        public object Get(GetDeviceOptions request)
-        {
-            return _deviceManager.GetDeviceOptions(request.Id);
-        }
-
-        public object Get(GetCameraUploads request)
-        {
-            return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
-        }
-
-        public void Delete(DeleteDevice request)
-        {
-            var sessions = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                DeviceId = request.Id
-
-            }).Items;
-
-            foreach (var session in sessions)
-            {
-                _sessionManager.Logout(session);
-            }
-        }
-
-        public Task Post(PostCameraUpload request)
-        {
-            var deviceId = Request.QueryString["DeviceId"];
-            var album = Request.QueryString["Album"];
-            var id = Request.QueryString["Id"];
-            var name = Request.QueryString["Name"];
-            var req = Request.Response.HttpContext.Request;
-
-            if (req.HasFormContentType)
-            {
-                var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0];
-
-                return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo
-                {
-                    MimeType = file.ContentType,
-                    Album = album,
-                    Name = name,
-                    Id = id
-                });
-            }
-
-            return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
-            {
-                MimeType = Request.ContentType,
-                Album = album,
-                Name = name,
-                Id = id
-            });
-        }
-    }
-}

From 440f060da6cfa8336d51bd05b723d67cfcf168eb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 19:36:18 -0600
Subject: [PATCH 024/463] Fix Authenticated Roles

---
 Jellyfin.Api/Controllers/DevicesController.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 7407c44878..a9dcfb955a 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -48,6 +48,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">/// Gets or sets the user identifier.</param>
         /// <returns>Device Infos.</returns>
         [HttpGet]
+        [Authenticated(Roles = "Admin")]
         [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
@@ -70,6 +71,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">Device Id.</param>
         /// <returns>Device Info.</returns>
         [HttpGet("Info")]
+        [Authenticated(Roles = "Admin")]
         [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
@@ -97,6 +99,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">Device Id.</param>
         /// <returns>Device Info.</returns>
         [HttpGet("Options")]
+        [Authenticated(Roles = "Admin")]
         [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
@@ -125,6 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="deviceOptions">Device Options.</param>
         /// <returns>Status.</returns>
         [HttpPost("Options")]
+        [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]

From 16cae23bbee14a7398d39014973b1a476e1ca57c Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Sun, 19 Apr 2020 21:06:28 -0600
Subject: [PATCH 025/463] Add response type annotations, return IActionResult
 to handle errors

---
 .../Controllers/NotificationsController.cs    | 79 ++++++++++++++-----
 1 file changed, 59 insertions(+), 20 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index d9a5c5e316..76b025fa16 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Notifications;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -42,13 +43,14 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
-        public NotificationResultDto GetNotifications(
+        [ProducesResponseType(typeof(IEnumerable<NotificationResultDto>), StatusCodes.Status200OK)]
+        public IActionResult GetNotifications(
             [FromRoute] string userId,
             [FromQuery] bool? isRead,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit)
         {
-            return new NotificationResultDto();
+            return Ok(new NotificationResultDto());
         }
 
         /// <summary>
@@ -57,10 +59,11 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user's ID.</param>
         /// <returns>Notifications summary for the user.</returns>
         [HttpGet("{UserID}/Summary")]
-        public NotificationsSummaryDto GetNotificationsSummary(
+        [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)]
+        public IActionResult GetNotificationsSummary(
             [FromRoute] string userId)
         {
-            return new NotificationsSummaryDto();
+            return Ok(new NotificationsSummaryDto());
         }
 
         /// <summary>
@@ -68,9 +71,18 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>All notification types.</returns>
         [HttpGet("Types")]
-        public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
+        [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetNotificationTypes()
         {
-            return _notificationManager.GetNotificationTypes();
+            try
+            {
+                return Ok(_notificationManager.GetNotificationTypes());
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
         }
 
         /// <summary>
@@ -78,9 +90,18 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>All notification services.</returns>
         [HttpGet("Services")]
-        public IEnumerable<NameIdPair> GetNotificationServices()
+        [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetNotificationServices()
         {
-            return _notificationManager.GetNotificationServices();
+            try
+            {
+                return Ok(_notificationManager.GetNotificationServices());
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
         }
 
         /// <summary>
@@ -90,24 +111,36 @@ namespace Jellyfin.Api.Controllers
         /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
+        /// <returns>Status.</returns>
         [HttpPost("Admin")]
-        public void CreateAdminNotification(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult CreateAdminNotification(
             [FromQuery] string name,
             [FromQuery] string description,
             [FromQuery] string? url,
             [FromQuery] NotificationLevel? level)
         {
-            var notification = new NotificationRequest
+            try
             {
-                Name = name,
-                Description = description,
-                Url = url,
-                Level = level ?? NotificationLevel.Normal,
-                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
-                Date = DateTime.UtcNow,
-            };
+                var notification = new NotificationRequest
+                {
+                    Name = name,
+                    Description = description,
+                    Url = url,
+                    Level = level ?? NotificationLevel.Normal,
+                    UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
+                    Date = DateTime.UtcNow,
+                };
+
+                _notificationManager.SendNotification(notification, CancellationToken.None);
 
-            _notificationManager.SendNotification(notification, CancellationToken.None);
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
         }
 
         /// <summary>
@@ -115,11 +148,14 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
+        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Read")]
-        public void SetRead(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IActionResult SetRead(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
+            return Ok();
         }
 
         /// <summary>
@@ -127,11 +163,14 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
+        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Unread")]
-        public void SetUnread(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IActionResult SetUnread(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
+            return Ok();
         }
     }
 }

From 688240151bae0f333cd329572b3774954d13ebae Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Mon, 20 Apr 2020 00:00:00 -0600
Subject: [PATCH 026/463] Enable nullable reference types on new class, remove
 unnecessary documenation and return types

---
 Jellyfin.Api/Controllers/NotificationsController.cs   | 11 ++---------
 .../Models/NotificationDtos/NotificationResultDto.cs  |  2 ++
 2 files changed, 4 insertions(+), 9 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 76b025fa16..c0c2be626b 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -72,7 +72,6 @@ namespace Jellyfin.Api.Controllers
         /// <returns>All notification types.</returns>
         [HttpGet("Types")]
         [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetNotificationTypes()
         {
             try
@@ -91,7 +90,6 @@ namespace Jellyfin.Api.Controllers
         /// <returns>All notification services.</returns>
         [HttpGet("Services")]
         [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetNotificationServices()
         {
             try
@@ -114,7 +112,6 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult CreateAdminNotification(
             [FromQuery] string name,
             [FromQuery] string description,
@@ -148,14 +145,12 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
-        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Read")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult SetRead(
+        public void SetRead(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
-            return Ok();
         }
 
         /// <summary>
@@ -163,14 +158,12 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
-        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Unread")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult SetUnread(
+        public void SetUnread(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
-            return Ok();
         }
     }
 }
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
index 64e92bd83a..e34e176cb9 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 

From fff2a40ffc4e5010b26143185c68d221225c1a22 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 20 Apr 2020 07:24:13 -0600
Subject: [PATCH 027/463] Remove StringEnumConverter

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index a4f078b5b3..92bacb4400 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -80,9 +80,6 @@ namespace Jellyfin.Server.Extensions
                 {
                     // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
                     options.JsonSerializerOptions.PropertyNamingPolicy = null;
-
-                    // Accept string enums
-                    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
                 })
                 .AddControllersAsServices();
         }

From e151d539f2041fb249af82118bde1168d1859c6b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 20 Apr 2020 13:06:29 -0600
Subject: [PATCH 028/463] Move ImageByNameService to Jellyfin.Api

---
 .../Images/ImageByNameController.cs           | 261 +++++++++++++++++
 MediaBrowser.Api/Images/ImageByNameService.cs | 277 ------------------
 2 files changed, 261 insertions(+), 277 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/Images/ImageByNameController.cs
 delete mode 100644 MediaBrowser.Api/Images/ImageByNameService.cs

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
new file mode 100644
index 0000000000..a14e2403c4
--- /dev/null
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -0,0 +1,261 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers.Images
+{
+    /// <summary>
+    ///     Images By Name Controller.
+    /// </summary>
+    [Route("Images")]
+    [Authenticated]
+    public class ImageByNameController : BaseJellyfinApiController
+    {
+        private readonly IServerApplicationPaths _applicationPaths;
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        ///     Initializes a new instance of the <see cref="ImageByNameController" /> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param>
+        public ImageByNameController(
+            IServerConfigurationManager serverConfigurationManager,
+            IFileSystem fileSystem)
+        {
+            _applicationPaths = serverConfigurationManager.ApplicationPaths;
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        ///     Get all general images.
+        /// </summary>
+        /// <returns>General images.</returns>
+        [HttpGet("General")]
+        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetGeneralImages()
+        {
+            try
+            {
+                return Ok(GetImageList(_applicationPaths.GeneralPath, false));
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Get General Image.
+        /// </summary>
+        /// <param name="name">The name of the image.</param>
+        /// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
+        /// <returns>Image Stream.</returns>
+        [HttpGet("General/{Name}/{Type}")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type)
+        {
+            try
+            {
+                var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
+                    ? "folder"
+                    : type;
+
+                var paths = BaseItem.SupportedImageExtensions
+                    .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList();
+
+                var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault();
+                if (path == null || !System.IO.File.Exists(path))
+                {
+                    return NotFound();
+                }
+
+                var contentType = MimeTypes.GetMimeType(path);
+                return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Get all general images.
+        /// </summary>
+        /// <returns>General images.</returns>
+        [HttpGet("Ratings")]
+        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetRatingImages()
+        {
+            try
+            {
+                return Ok(GetImageList(_applicationPaths.RatingsPath, false));
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Get rating image.
+        /// </summary>
+        /// <param name="theme">The theme to get the image from.</param>
+        /// <param name="name">The name of the image.</param>
+        /// <returns>Image Stream.</returns>
+        [HttpGet("Ratings/{Theme}/{Name}")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetRatingImage(
+            [FromRoute] string theme,
+            [FromRoute] string name)
+        {
+            try
+            {
+                return GetImageFile(_applicationPaths.RatingsPath, theme, name);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Get all media info images.
+        /// </summary>
+        /// <returns>Media Info images.</returns>
+        [HttpGet("MediaInfo")]
+        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetMediaInfoImages()
+        {
+            try
+            {
+                return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Get media info image.
+        /// </summary>
+        /// <param name="theme">The theme to get the image from.</param>
+        /// <param name="name">The name of the image.</param>
+        /// <returns>Image Stream.</returns>
+        [HttpGet("MediaInfo/{Theme}/{Name}")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetMediaInfoImage(
+            [FromRoute] string theme,
+            [FromRoute] string name)
+        {
+            try
+            {
+                return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        ///     Internal FileHelper.
+        /// </summary>
+        /// <param name="basePath">Path to begin search.</param>
+        /// <param name="theme">Theme to search.</param>
+        /// <param name="name">File name to search for.</param>
+        /// <returns>Image Stream.</returns>
+        private IActionResult GetImageFile(string basePath, string theme, string name)
+        {
+            var themeFolder = Path.Combine(basePath, theme);
+            if (Directory.Exists(themeFolder))
+            {
+                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
+                    .FirstOrDefault(System.IO.File.Exists);
+
+                if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
+                {
+                    var contentType = MimeTypes.GetMimeType(path);
+                    return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+                }
+            }
+
+            var allFolder = Path.Combine(basePath, "all");
+            if (Directory.Exists(allFolder))
+            {
+                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
+                    .FirstOrDefault(System.IO.File.Exists);
+
+                if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
+                {
+                    var contentType = MimeTypes.GetMimeType(path);
+                    return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+                }
+            }
+
+            return NotFound();
+        }
+
+        private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
+        {
+            try
+            {
+                return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
+                    .Select(i => new ImageByNameInfo
+                    {
+                        Name = _fileSystem.GetFileNameWithoutExtension(i),
+                        FileLength = i.Length,
+
+                        // For themeable images, use the Theme property
+                        // For general images, the same object structure is fine,
+                        // but it's not owned by a theme, so call it Context
+                        Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
+                        Context = supportsThemes ? null : GetThemeName(i.FullName, path),
+                        Format = i.Extension.ToLowerInvariant().TrimStart('.')
+                    })
+                    .OrderBy(i => i.Name)
+                    .ToList();
+            }
+            catch (IOException)
+            {
+                return new List<ImageByNameInfo>();
+            }
+        }
+
+        private string GetThemeName(string path, string rootImagePath)
+        {
+            var parentName = Path.GetDirectoryName(path);
+
+            if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            parentName = Path.GetFileName(parentName);
+
+            return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs
deleted file mode 100644
index 45b7d0c100..0000000000
--- a/MediaBrowser.Api/Images/ImageByNameService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
-    /// <summary>
-    /// Class GetGeneralImage
-    /// </summary>
-    [Route("/Images/General/{Name}/{Type}", "GET", Summary = "Gets a general image by name")]
-    public class GetGeneralImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Type", Description = "Image Type (primary, backdrop, logo, etc).", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Type { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetRatingImage
-    /// </summary>
-    [Route("/Images/Ratings/{Theme}/{Name}", "GET", Summary = "Gets a rating image by name")]
-    public class GetRatingImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the theme.
-        /// </summary>
-        /// <value>The theme.</value>
-        [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Theme { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetMediaInfoImage
-    /// </summary>
-    [Route("/Images/MediaInfo/{Theme}/{Name}", "GET", Summary = "Gets a media info image by name")]
-    public class GetMediaInfoImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the theme.
-        /// </summary>
-        /// <value>The theme.</value>
-        [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Theme { get; set; }
-    }
-
-    [Route("/Images/MediaInfo", "GET", Summary = "Gets all media info image by name")]
-    [Authenticated]
-    public class GetMediaInfoImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    [Route("/Images/Ratings", "GET", Summary = "Gets all rating images by name")]
-    [Authenticated]
-    public class GetRatingImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    [Route("/Images/General", "GET", Summary = "Gets all general images by name")]
-    [Authenticated]
-    public class GetGeneralImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    /// <summary>
-    /// Class ImageByNameService
-    /// </summary>
-    public class ImageByNameService : BaseApiService
-    {
-        /// <summary>
-        /// The _app paths
-        /// </summary>
-        private readonly IServerApplicationPaths _appPaths;
-
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImageByNameService" /> class.
-        /// </summary>
-        public ImageByNameService(
-            ILogger<ImageByNameService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory resultFactory,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, resultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _fileSystem = fileSystem;
-        }
-
-        public object Get(GetMediaInfoImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath, true));
-        }
-
-        public object Get(GetRatingImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.RatingsPath, true));
-        }
-
-        public object Get(GetGeneralImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.GeneralPath, false));
-        }
-
-        private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
-        {
-            try
-            {
-                return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
-                    .Select(i => new ImageByNameInfo
-                    {
-                        Name = _fileSystem.GetFileNameWithoutExtension(i),
-                        FileLength = i.Length,
-
-                        // For themeable images, use the Theme property
-                        // For general images, the same object structure is fine,
-                        // but it's not owned by a theme, so call it Context
-                        Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
-                        Context = supportsThemes ? null : GetThemeName(i.FullName, path),
-
-                        Format = i.Extension.ToLowerInvariant().TrimStart('.')
-                    })
-                    .OrderBy(i => i.Name)
-                    .ToList();
-            }
-            catch (IOException)
-            {
-                return new List<ImageByNameInfo>();
-            }
-        }
-
-        private string GetThemeName(string path, string rootImagePath)
-        {
-            var parentName = Path.GetDirectoryName(path);
-
-            if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
-            {
-                return null;
-            }
-
-            parentName = Path.GetFileName(parentName);
-
-            return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ?
-                null :
-                parentName;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetGeneralImage request)
-        {
-            var filename = string.Equals(request.Type, "primary", StringComparison.OrdinalIgnoreCase)
-                               ? "folder"
-                               : request.Type;
-
-            var paths = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(_appPaths.GeneralPath, request.Name, filename + i)).ToList();
-
-            var path = paths.FirstOrDefault(File.Exists) ?? paths.FirstOrDefault();
-
-            return ResultFactory.GetStaticFileResult(Request, path);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRatingImage request)
-        {
-            var themeFolder = Path.Combine(_appPaths.RatingsPath, request.Theme);
-
-            if (Directory.Exists(themeFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions
-                    .Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            var allFolder = Path.Combine(_appPaths.RatingsPath, "all");
-
-            if (Directory.Exists(allFolder))
-            {
-                // Avoid implicitly captured closure
-                var currentRequest = request;
-
-                var path = BaseItem.SupportedImageExtensions
-                    .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetMediaInfoImage request)
-        {
-            var themeFolder = Path.Combine(_appPaths.MediaInfoImagesPath, request.Theme);
-
-            if (Directory.Exists(themeFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            var allFolder = Path.Combine(_appPaths.MediaInfoImagesPath, "all");
-
-            if (Directory.Exists(allFolder))
-            {
-                // Avoid implicitly captured closure
-                var currentRequest = request;
-
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
-        }
-    }
-}

From 376619369d8b1e889475da1191092f43e7f26ae6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 20 Apr 2020 13:12:35 -0600
Subject: [PATCH 029/463] fix build

---
 Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index a14e2403c4..3097296051 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers.Images
             }
         }
 
-        private string GetThemeName(string path, string rootImagePath)
+        private string? GetThemeName(string path, string rootImagePath)
         {
             var parentName = Path.GetDirectoryName(path);
 

From 766d2ee413a15c682c0d687619064caf98f9031c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 20 Apr 2020 14:21:06 -0600
Subject: [PATCH 030/463] Move RemoteImageService to Jellyfin.API

---
 .../Images/RemoteImageController.cs           | 290 +++++++++++++++++
 MediaBrowser.Api/Images/RemoteImageService.cs | 295 ------------------
 2 files changed, 290 insertions(+), 295 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/Images/RemoteImageController.cs
 delete mode 100644 MediaBrowser.Api/Images/RemoteImageService.cs

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
new file mode 100644
index 0000000000..66479582da
--- /dev/null
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -0,0 +1,290 @@
+#nullable enable
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers.Images
+{
+    /// <summary>
+    /// Remote Images Controller.
+    /// </summary>
+    [Route("Images")]
+    [Authenticated]
+    public class RemoteImageController : BaseJellyfinApiController
+    {
+        private readonly IProviderManager _providerManager;
+        private readonly IServerApplicationPaths _applicationPaths;
+        private readonly IHttpClient _httpClient;
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RemoteImageController"/> class.
+        /// </summary>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+        /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public RemoteImageController(
+            IProviderManager providerManager,
+            IServerApplicationPaths applicationPaths,
+            IHttpClient httpClient,
+            ILibraryManager libraryManager)
+        {
+            _providerManager = providerManager;
+            _applicationPaths = applicationPaths;
+            _httpClient = httpClient;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets available remote images for an item.
+        /// </summary>
+        /// <param name="id">Item Id.</param>
+        /// <param name="type">The image type.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="providerName">Optional. The image provider to use.</param>
+        /// <param name="includeAllLanguages">Optinal. Include all languages.</param>
+        /// <returns>Remote Image Result.</returns>
+        [HttpGet("{Id}/RemoteImages")]
+        [ProducesResponseType(typeof(RemoteImageResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+        public async Task<IActionResult> GetRemoteImages(
+            [FromRoute] string id,
+            [FromQuery] ImageType? type,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string providerName,
+            [FromQuery] bool includeAllLanguages)
+        {
+            try
+            {
+                var item = _libraryManager.GetItemById(id);
+                if (item == null)
+                {
+                    return NotFound();
+                }
+
+                var images = await _providerManager.GetAvailableRemoteImages(
+                        item,
+                        new RemoteImageQuery
+                        {
+                            ProviderName = providerName,
+                            IncludeAllLanguages = includeAllLanguages,
+                            IncludeDisabledProviders = true,
+                            ImageType = type
+                        }, CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                var imageArray = images.ToArray();
+                var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
+                if (type.HasValue)
+                {
+                    allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
+                }
+
+                var result = new RemoteImageResult
+                {
+                    TotalRecordCount = imageArray.Length,
+                    Providers = allProviders.Select(o => o.Name)
+                        .Distinct(StringComparer.OrdinalIgnoreCase)
+                        .ToArray()
+                };
+
+                if (startIndex.HasValue)
+                {
+                    imageArray = imageArray.Skip(startIndex.Value).ToArray();
+                }
+
+                if (limit.HasValue)
+                {
+                    imageArray = imageArray.Take(limit.Value).ToArray();
+                }
+
+                result.Images = imageArray;
+                return Ok(result);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Gets available remote image providers for an item.
+        /// </summary>
+        /// <param name="id">Item Id.</param>
+        /// <returns>List of providers.</returns>
+        [HttpGet("{Id}/RemoteImages/Providers")]
+        [ProducesResponseType(typeof(ImageProviderInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public IActionResult GetRemoteImageProviders([FromRoute] string id)
+        {
+            try
+            {
+                var item = _libraryManager.GetItemById(id);
+                if (item == null)
+                {
+                    return NotFound();
+                }
+
+                var providers = _providerManager.GetRemoteImageProviderInfo(item);
+                return Ok(providers);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Gets a remote image.
+        /// </summary>
+        /// <param name="imageUrl">The image url.</param>
+        /// <returns>Image Stream.</returns>
+        [HttpGet("Remote")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public async Task<IActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
+        {
+            try
+            {
+                var urlHash = imageUrl.GetMD5();
+                var pointerCachePath = GetFullCachePath(urlHash.ToString());
+
+                string? contentPath = null;
+                bool hasFile = false;
+
+                try
+                {
+                    contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+                    if (System.IO.File.Exists(contentPath))
+                    {
+                        hasFile = true;
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+                    // Means the file isn't cached yet
+                }
+                catch (IOException)
+                {
+                    // Means the file isn't cached yet
+                }
+
+                if (!hasFile)
+                {
+                    await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
+                    contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+                }
+
+                if (string.IsNullOrEmpty(contentPath))
+                {
+                    return NotFound();
+                }
+
+                var contentType = MimeTypes.GetMimeType(contentPath);
+                return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Downloads a remote image for an item.
+        /// </summary>
+        /// <param name="id">Item Id.</param>
+        /// <param name="type">The image type.</param>
+        /// <param name="imageUrl">The image url.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("{Id}/RemoteImages/Download")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public async Task<IActionResult> DownloadRemoteImage(
+            [FromRoute] string id,
+            [FromQuery, BindRequired] ImageType type,
+            [FromQuery] string imageUrl)
+        {
+            try
+            {
+                var item = _libraryManager.GetItemById(id);
+                if (item == null)
+                {
+                    return NotFound();
+                }
+
+                await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+                return Ok();
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// Gets the full cache path.
+        /// </summary>
+        /// <param name="filename">The filename.</param>
+        /// <returns>System.String.</returns>
+        private string GetFullCachePath(string filename)
+        {
+            return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
+        }
+
+        /// <summary>
+        /// Downloads the image.
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <param name="urlHash">The URL hash.</param>
+        /// <param name="pointerCachePath">The pointer cache path.</param>
+        /// <returns>Task.</returns>
+        private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
+        {
+            using var result = await _httpClient.GetResponse(new HttpRequestOptions
+            {
+                Url = url,
+                BufferContent = false
+            }).ConfigureAwait(false);
+            var ext = result.ContentType.Split('/').Last();
+
+            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+            using (var stream = result.Content)
+            {
+                using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+                await stream.CopyToAsync(filestream).ConfigureAwait(false);
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+            await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
+                .ConfigureAwait(false);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
deleted file mode 100644
index 222bb34d31..0000000000
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ /dev/null
@@ -1,295 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
-    public class BaseRemoteImageRequest : IReturn<RemoteImageResult>
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public ImageType? Type { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeAllLanguages { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")]
-    [Authenticated]
-    public class GetRemoteImages : BaseRemoteImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")]
-    [Authenticated]
-    public class GetRemoteImageProviders : IReturn<List<ImageProviderInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    public class BaseDownloadRemoteImage : IReturnVoid
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public ImageType Type { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ImageUrl { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")]
-    [Authenticated(Roles = "Admin")]
-    public class DownloadRemoteImage : BaseDownloadRemoteImage
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Images/Remote", "GET", Summary = "Gets a remote image")]
-    public class GetRemoteImage
-    {
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ImageUrl { get; set; }
-    }
-
-    public class RemoteImageService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IHttpClient _httpClient;
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ILibraryManager _libraryManager;
-
-        public RemoteImageService(
-            ILogger<RemoteImageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            IServerApplicationPaths appPaths,
-            IHttpClient httpClient,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _appPaths = appPaths;
-            _httpClient = httpClient;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetRemoteImageProviders request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var result = GetImageProviders(item);
-
-            return ToOptimizedResult(result);
-        }
-
-        private List<ImageProviderInfo> GetImageProviders(BaseItem item)
-        {
-            return _providerManager.GetRemoteImageProviderInfo(item).ToList();
-        }
-
-        public async Task<object> Get(GetRemoteImages request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery
-            {
-                ProviderName = request.ProviderName,
-                IncludeAllLanguages = request.IncludeAllLanguages,
-                IncludeDisabledProviders = true,
-                ImageType = request.Type
-
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            var imagesList = images.ToArray();
-
-            var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
-
-            if (request.Type.HasValue)
-            {
-                allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value));
-            }
-
-            var result = new RemoteImageResult
-            {
-                TotalRecordCount = imagesList.Length,
-                Providers = allProviders.Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToArray()
-            };
-
-            if (request.StartIndex.HasValue)
-            {
-                imagesList = imagesList.Skip(request.StartIndex.Value)
-                    .ToArray();
-            }
-
-            if (request.Limit.HasValue)
-            {
-                imagesList = imagesList.Take(request.Limit.Value)
-                    .ToArray();
-            }
-
-            result.Images = imagesList;
-
-            return result;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(DownloadRemoteImage request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return DownloadRemoteImage(item, request);
-        }
-
-        /// <summary>
-        /// Downloads the remote image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="request">The request.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request)
-        {
-            await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false);
-
-            item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetRemoteImage request)
-        {
-            var urlHash = request.ImageUrl.GetMD5();
-            var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
-            string contentPath;
-
-            try
-            {
-                contentPath = File.ReadAllText(pointerCachePath);
-
-                if (File.Exists(contentPath))
-                {
-                    return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // Means the file isn't cached yet
-            }
-            catch (IOException)
-            {
-                // Means the file isn't cached yet
-            }
-
-            await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
-            // Read the pointer file again
-            contentPath = File.ReadAllText(pointerCachePath);
-
-            return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Downloads the image.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <param name="urlHash">The URL hash.</param>
-        /// <param name="pointerCachePath">The pointer cache path.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
-        {
-            using var result = await _httpClient.GetResponse(new HttpRequestOptions
-            {
-                Url = url,
-                BufferContent = false
-
-            }).ConfigureAwait(false);
-            var ext = result.ContentType.Split('/').Last();
-
-            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            using (var stream = result.Content)
-            {
-                using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-                await stream.CopyToAsync(filestream).ConfigureAwait(false);
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
-            File.WriteAllText(pointerCachePath, fullCachePath);
-        }
-
-        /// <summary>
-        /// Gets the full cache path.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        private string GetFullCachePath(string filename)
-        {
-            return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-        }
-    }
-}

From 67efcbee05fe7917aaff11fd27235fb952938434 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Mon, 20 Apr 2020 20:16:58 -0600
Subject: [PATCH 031/463] Remove error handlers, to be implemented at a global
 level in a separate PR

---
 .../Controllers/NotificationsController.cs    | 47 +++++--------------
 1 file changed, 12 insertions(+), 35 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index c0c2be626b..2a41f6020e 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -74,14 +74,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
         public IActionResult GetNotificationTypes()
         {
-            try
-            {
-                return Ok(_notificationManager.GetNotificationTypes());
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(_notificationManager.GetNotificationTypes());
         }
 
         /// <summary>
@@ -92,14 +85,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
         public IActionResult GetNotificationServices()
         {
-            try
-            {
-                return Ok(_notificationManager.GetNotificationServices());
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(_notificationManager.GetNotificationServices());
         }
 
         /// <summary>
@@ -112,32 +98,23 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult CreateAdminNotification(
+        public void CreateAdminNotification(
             [FromQuery] string name,
             [FromQuery] string description,
             [FromQuery] string? url,
             [FromQuery] NotificationLevel? level)
         {
-            try
+            var notification = new NotificationRequest
             {
-                var notification = new NotificationRequest
-                {
-                    Name = name,
-                    Description = description,
-                    Url = url,
-                    Level = level ?? NotificationLevel.Normal,
-                    UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
-                    Date = DateTime.UtcNow,
-                };
-
-                _notificationManager.SendNotification(notification, CancellationToken.None);
+                Name = name,
+                Description = description,
+                Url = url,
+                Level = level ?? NotificationLevel.Normal,
+                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
+                Date = DateTime.UtcNow,
+            };
 
-                return Ok();
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            _notificationManager.SendNotification(notification, CancellationToken.None);
         }
 
         /// <summary>

From 6c8e1d37bd49339d298c46c24cddf8e858b334c8 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Mon, 20 Apr 2020 23:53:09 -0600
Subject: [PATCH 032/463] Remove more unnecessary IActionResult

---
 .../Controllers/NotificationsController.cs    | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 2a41f6020e..932b91d55c 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -43,14 +43,14 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
-        [ProducesResponseType(typeof(IEnumerable<NotificationResultDto>), StatusCodes.Status200OK)]
-        public IActionResult GetNotifications(
+        [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)]
+        public NotificationResultDto GetNotifications(
             [FromRoute] string userId,
             [FromQuery] bool? isRead,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit)
         {
-            return Ok(new NotificationResultDto());
+            return new NotificationResultDto();
         }
 
         /// <summary>
@@ -60,10 +60,10 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Notifications summary for the user.</returns>
         [HttpGet("{UserID}/Summary")]
         [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)]
-        public IActionResult GetNotificationsSummary(
+        public NotificationsSummaryDto GetNotificationsSummary(
             [FromRoute] string userId)
         {
-            return Ok(new NotificationsSummaryDto());
+            return new NotificationsSummaryDto();
         }
 
         /// <summary>
@@ -71,10 +71,10 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>All notification types.</returns>
         [HttpGet("Types")]
-        [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
-        public IActionResult GetNotificationTypes()
+        [ProducesResponseType(typeof(IEnumerable<NotificationTypeInfo>), StatusCodes.Status200OK)]
+        public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
         {
-            return Ok(_notificationManager.GetNotificationTypes());
+            return _notificationManager.GetNotificationTypes();
         }
 
         /// <summary>
@@ -83,9 +83,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>All notification services.</returns>
         [HttpGet("Services")]
         [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
-        public IActionResult GetNotificationServices()
+        public IEnumerable<NameIdPair> GetNotificationServices()
         {
-            return Ok(_notificationManager.GetNotificationServices());
+            return _notificationManager.GetNotificationServices();
         }
 
         /// <summary>

From dae69657108f90de54166a670c47a6dff2dae139 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Tue, 21 Apr 2020 00:24:35 -0600
Subject: [PATCH 033/463] Remove documentation of void return type

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 932b91d55c..c1d9e32515 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -95,7 +95,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
-        /// <returns>Status.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public void CreateAdminNotification(

From 1175ce3f97fdebc6fdb489ce65deaac59c7b7f87 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:36:22 -0600
Subject: [PATCH 034/463] Add Exception Middleware

---
 .../Models/ExceptionDtos/ExceptionDto.cs      | 14 +++++
 .../ApiApplicationBuilderExtensions.cs        | 11 ++++
 .../Middleware/ExceptionMiddleware.cs         | 60 +++++++++++++++++++
 Jellyfin.Server/Startup.cs                    |  2 +
 4 files changed, 87 insertions(+)
 create mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs
 create mode 100644 Jellyfin.Server/Middleware/ExceptionMiddleware.cs

diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs
new file mode 100644
index 0000000000..d2b48d4ae5
--- /dev/null
+++ b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs
@@ -0,0 +1,14 @@
+namespace Jellyfin.Api.Models.ExceptionDtos
+{
+    /// <summary>
+    /// Exception Dto.
+    /// Used for graceful handling of API exceptions.
+    /// </summary>
+    public class ExceptionDto
+    {
+        /// <summary>
+        /// Gets or sets exception message.
+        /// </summary>
+        public string Message { get; set; }
+    }
+}
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index db06eb4552..6c105ab65b 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Server.Middleware;
 using Microsoft.AspNetCore.Builder;
 
 namespace Jellyfin.Server.Extensions
@@ -23,5 +24,15 @@ namespace Jellyfin.Server.Extensions
                 c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1");
             });
         }
+
+        /// <summary>
+        /// Adds exception middleware to the application pipeline.
+        /// </summary>
+        /// <param name="applicationBuilder">The application builder.</param>
+        /// <returns>The updated application builder.</returns>
+        public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder)
+        {
+            return applicationBuilder.UseMiddleware<ExceptionMiddleware>();
+        }
     }
 }
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
new file mode 100644
index 0000000000..39aace95d2
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.ExceptionDtos;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Middleware
+{
+    /// <summary>
+    /// Exception Middleware.
+    /// </summary>
+    public class ExceptionMiddleware
+    {
+        private readonly RequestDelegate _next;
+        private readonly ILogger<ExceptionMiddleware> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
+        /// </summary>
+        /// <param name="next">Next request delegate.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
+        {
+            _next = next ?? throw new ArgumentNullException(nameof(next));
+            _logger = loggerFactory.CreateLogger<ExceptionMiddleware>() ??
+                      throw new ArgumentNullException(nameof(loggerFactory));
+        }
+
+        /// <summary>
+        /// Invoke request.
+        /// </summary>
+        /// <param name="context">Request context.</param>
+        /// <returns>Task.</returns>
+        public async Task Invoke(HttpContext context)
+        {
+            try
+            {
+                await _next(context).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                if (context.Response.HasStarted)
+                {
+                    _logger.LogWarning("The response has already started, the exception middleware will not be executed.");
+                    throw;
+                }
+
+                var exceptionBody = new ExceptionDto { Message = ex.Message };
+                var exceptionJson = JsonSerializer.Serialize(exceptionBody);
+
+                context.Response.Clear();
+                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
+                // TODO switch between PascalCase and camelCase
+                context.Response.ContentType = "application/json";
+                await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false);
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 4d7d56e9d4..7a632f6c44 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -58,6 +58,8 @@ namespace Jellyfin.Server
                 app.UseDeveloperExceptionPage();
             }
 
+            app.UseExceptionMiddleware();
+
             app.UseWebSockets();
 
             app.UseResponseCompression();

From 08eba82bb7bebe277f6b106fa48994bb98c3dd41 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:52:33 -0600
Subject: [PATCH 035/463] Remove exception handler

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index f4c1a761fb..aeeaf5cbdc 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -79,10 +79,6 @@ namespace Jellyfin.Api.Controllers
             {
                 return StatusCode(StatusCodes.Status404NotFound, e.Message);
             }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
         }
     }
 }

From 5ef71d592b84b73290e3e7a34cd7fa8b9f337f50 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:55:01 -0600
Subject: [PATCH 036/463] Remove exception handler

---
 Jellyfin.Api/Controllers/DevicesController.cs | 143 ++++++------------
 1 file changed, 44 insertions(+), 99 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index a9dcfb955a..5dc3f27ee1 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -53,16 +53,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
-            try
-            {
-                var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
-                var devices = _deviceManager.GetDevices(deviceQuery);
-                return Ok(devices);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
+            var devices = _deviceManager.GetDevices(deviceQuery);
+            return Ok(devices);
         }
 
         /// <summary>
@@ -77,20 +70,13 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id)
         {
-            try
-            {
-                var deviceInfo = _deviceManager.GetDevice(id);
-                if (deviceInfo == null)
-                {
-                    return NotFound();
-                }
-
-                return Ok(deviceInfo);
-            }
-            catch (Exception e)
+            var deviceInfo = _deviceManager.GetDevice(id);
+            if (deviceInfo == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            return Ok(deviceInfo);
         }
 
         /// <summary>
@@ -105,20 +91,13 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id)
         {
-            try
+            var deviceInfo = _deviceManager.GetDeviceOptions(id);
+            if (deviceInfo == null)
             {
-                var deviceInfo = _deviceManager.GetDeviceOptions(id);
-                if (deviceInfo == null)
-                {
-                    return NotFound();
-                }
-
-                return Ok(deviceInfo);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            return Ok(deviceInfo);
         }
 
         /// <summary>
@@ -136,21 +115,14 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, BindRequired] string id,
             [FromBody, BindRequired] DeviceOptions deviceOptions)
         {
-            try
-            {
-                var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
-                if (existingDeviceOptions == null)
-                {
-                    return NotFound();
-                }
-
-                _deviceManager.UpdateDeviceOptions(id, deviceOptions);
-                return Ok();
-            }
-            catch (Exception e)
+            var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
+            if (existingDeviceOptions == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            _deviceManager.UpdateDeviceOptions(id, deviceOptions);
+            return Ok();
         }
 
         /// <summary>
@@ -163,21 +135,14 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult DeleteDevice([FromQuery, BindRequired] string id)
         {
-            try
-            {
-                var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
-
-                foreach (var session in sessions)
-                {
-                    _sessionManager.Logout(session);
-                }
+            var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
 
-                return Ok();
-            }
-            catch (Exception e)
+            foreach (var session in sessions)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                _sessionManager.Logout(session);
             }
+
+            return Ok();
         }
 
         /// <summary>
@@ -190,15 +155,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetCameraUploads([FromQuery, BindRequired] string id)
         {
-            try
-            {
-                var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
-                return Ok(uploadHistory);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
+            return Ok(uploadHistory);
         }
 
         /// <summary>
@@ -219,46 +177,33 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, BindRequired] string name,
             [FromQuery, BindRequired] string id)
         {
-            try
-            {
-                Stream fileStream;
-                string contentType;
+            Stream fileStream;
+            string contentType;
 
-                if (Request.HasFormContentType)
+            if (Request.HasFormContentType)
+            {
+                if (Request.Form.Files.Any())
                 {
-                    if (Request.Form.Files.Any())
-                    {
-                        fileStream = Request.Form.Files[0].OpenReadStream();
-                        contentType = Request.Form.Files[0].ContentType;
-                    }
-                    else
-                    {
-                        return BadRequest();
-                    }
+                    fileStream = Request.Form.Files[0].OpenReadStream();
+                    contentType = Request.Form.Files[0].ContentType;
                 }
                 else
                 {
-                    fileStream = Request.Body;
-                    contentType = Request.ContentType;
+                    return BadRequest();
                 }
-
-                await _deviceManager.AcceptCameraUpload(
-                    deviceId,
-                    fileStream,
-                    new LocalFileInfo
-                    {
-                        MimeType = contentType,
-                        Album = album,
-                        Name = name,
-                        Id = id
-                    }).ConfigureAwait(false);
-
-                return Ok();
             }
-            catch (Exception e)
+            else
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                fileStream = Request.Body;
+                contentType = Request.ContentType;
             }
+
+            await _deviceManager.AcceptCameraUpload(
+                deviceId,
+                fileStream,
+                new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false);
+
+            return Ok();
         }
     }
 }

From 04119c0d409342050cb7624f025a21985e10a412 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:55:57 -0600
Subject: [PATCH 037/463] Remove exception handler

---
 .../DisplayPreferencesController.cs           | 51 +++++++------------
 1 file changed, 18 insertions(+), 33 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 0554091b45..e15e9c4be6 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,6 +1,5 @@
 #nullable enable
 
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.Threading;
 using MediaBrowser.Controller.Net;
@@ -45,20 +44,13 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] [Required] string userId,
             [FromQuery] [Required] string client)
         {
-            try
+            var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
+            if (result == null)
             {
-                var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
-                if (result == null)
-                {
-                    return NotFound();
-                }
-
-                return Ok(result);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            return Ok(result);
         }
 
         /// <summary>
@@ -80,30 +72,23 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, BindRequired] string client,
             [FromBody, BindRequired] DisplayPreferences displayPreferences)
         {
-            try
+            if (!ModelState.IsValid)
             {
-                if (!ModelState.IsValid)
-                {
-                    return BadRequest(ModelState);
-                }
-
-                if (displayPreferencesId == null)
-                {
-                    // do nothing.
-                }
-
-                _displayPreferencesRepository.SaveDisplayPreferences(
-                    displayPreferences,
-                    userId,
-                    client,
-                    CancellationToken.None);
-
-                return Ok();
+                return BadRequest(ModelState);
             }
-            catch (Exception e)
+
+            if (displayPreferencesId == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                // do nothing.
             }
+
+            _displayPreferencesRepository.SaveDisplayPreferences(
+                displayPreferences,
+                userId,
+                client,
+                CancellationToken.None);
+
+            return Ok();
         }
     }
 }

From 30609236ab58532d021e45edcdacd32d78aeca94 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:57:45 -0600
Subject: [PATCH 038/463] Remove exception handler

---
 .../Images/ImageByNameController.cs           | 74 ++++---------------
 1 file changed, 16 insertions(+), 58 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index 3097296051..4034c9e857 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -48,14 +48,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetGeneralImages()
         {
-            try
-            {
-                return Ok(GetImageList(_applicationPaths.GeneralPath, false));
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(GetImageList(_applicationPaths.GeneralPath, false));
         }
 
         /// <summary>
@@ -70,28 +63,21 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type)
         {
-            try
-            {
-                var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
-                    ? "folder"
-                    : type;
-
-                var paths = BaseItem.SupportedImageExtensions
-                    .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList();
+            var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
+                ? "folder"
+                : type;
 
-                var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault();
-                if (path == null || !System.IO.File.Exists(path))
-                {
-                    return NotFound();
-                }
+            var paths = BaseItem.SupportedImageExtensions
+                .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList();
 
-                var contentType = MimeTypes.GetMimeType(path);
-                return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
-            }
-            catch (Exception e)
+            var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault();
+            if (path == null || !System.IO.File.Exists(path))
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            var contentType = MimeTypes.GetMimeType(path);
+            return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
         }
 
         /// <summary>
@@ -103,14 +89,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetRatingImages()
         {
-            try
-            {
-                return Ok(GetImageList(_applicationPaths.RatingsPath, false));
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(GetImageList(_applicationPaths.RatingsPath, false));
         }
 
         /// <summary>
@@ -127,14 +106,7 @@ namespace Jellyfin.Api.Controllers.Images
             [FromRoute] string theme,
             [FromRoute] string name)
         {
-            try
-            {
-                return GetImageFile(_applicationPaths.RatingsPath, theme, name);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return GetImageFile(_applicationPaths.RatingsPath, theme, name);
         }
 
         /// <summary>
@@ -146,14 +118,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetMediaInfoImages()
         {
-            try
-            {
-                return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
         }
 
         /// <summary>
@@ -170,14 +135,7 @@ namespace Jellyfin.Api.Controllers.Images
             [FromRoute] string theme,
             [FromRoute] string name)
         {
-            try
-            {
-                return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
         }
 
         /// <summary>

From a6cd8526758386045a6895b0037f2199bdcb9003 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:58:54 -0600
Subject: [PATCH 039/463] Remove exception handler

---
 .../Images/RemoteImageController.cs           | 184 ++++++++----------
 1 file changed, 78 insertions(+), 106 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index 66479582da..8c7d21cd53 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -74,57 +74,50 @@ namespace Jellyfin.Api.Controllers.Images
             [FromQuery] string providerName,
             [FromQuery] bool includeAllLanguages)
         {
-            try
+            var item = _libraryManager.GetItemById(id);
+            if (item == null)
             {
-                var item = _libraryManager.GetItemById(id);
-                if (item == null)
-                {
-                    return NotFound();
-                }
-
-                var images = await _providerManager.GetAvailableRemoteImages(
-                        item,
-                        new RemoteImageQuery
-                        {
-                            ProviderName = providerName,
-                            IncludeAllLanguages = includeAllLanguages,
-                            IncludeDisabledProviders = true,
-                            ImageType = type
-                        }, CancellationToken.None)
-                    .ConfigureAwait(false);
-
-                var imageArray = images.ToArray();
-                var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
-                if (type.HasValue)
-                {
-                    allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
-                }
+                return NotFound();
+            }
 
-                var result = new RemoteImageResult
-                {
-                    TotalRecordCount = imageArray.Length,
-                    Providers = allProviders.Select(o => o.Name)
-                        .Distinct(StringComparer.OrdinalIgnoreCase)
-                        .ToArray()
-                };
+            var images = await _providerManager.GetAvailableRemoteImages(
+                    item,
+                    new RemoteImageQuery
+                    {
+                        ProviderName = providerName,
+                        IncludeAllLanguages = includeAllLanguages,
+                        IncludeDisabledProviders = true,
+                        ImageType = type
+                    }, CancellationToken.None)
+                .ConfigureAwait(false);
 
-                if (startIndex.HasValue)
-                {
-                    imageArray = imageArray.Skip(startIndex.Value).ToArray();
-                }
+            var imageArray = images.ToArray();
+            var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
+            if (type.HasValue)
+            {
+                allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
+            }
 
-                if (limit.HasValue)
-                {
-                    imageArray = imageArray.Take(limit.Value).ToArray();
-                }
+            var result = new RemoteImageResult
+            {
+                TotalRecordCount = imageArray.Length,
+                Providers = allProviders.Select(o => o.Name)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToArray()
+            };
 
-                result.Images = imageArray;
-                return Ok(result);
+            if (startIndex.HasValue)
+            {
+                imageArray = imageArray.Skip(startIndex.Value).ToArray();
             }
-            catch (Exception e)
+
+            if (limit.HasValue)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                imageArray = imageArray.Take(limit.Value).ToArray();
             }
+
+            result.Images = imageArray;
+            return Ok(result);
         }
 
         /// <summary>
@@ -138,21 +131,14 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetRemoteImageProviders([FromRoute] string id)
         {
-            try
+            var item = _libraryManager.GetItemById(id);
+            if (item == null)
             {
-                var item = _libraryManager.GetItemById(id);
-                if (item == null)
-                {
-                    return NotFound();
-                }
-
-                var providers = _providerManager.GetRemoteImageProviderInfo(item);
-                return Ok(providers);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            var providers = _providerManager.GetRemoteImageProviderInfo(item);
+            return Ok(providers);
         }
 
         /// <summary>
@@ -166,49 +152,42 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public async Task<IActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
         {
-            try
-            {
-                var urlHash = imageUrl.GetMD5();
-                var pointerCachePath = GetFullCachePath(urlHash.ToString());
+            var urlHash = imageUrl.GetMD5();
+            var pointerCachePath = GetFullCachePath(urlHash.ToString());
 
-                string? contentPath = null;
-                bool hasFile = false;
+            string? contentPath = null;
+            bool hasFile = false;
 
-                try
-                {
-                    contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
-                    if (System.IO.File.Exists(contentPath))
-                    {
-                        hasFile = true;
-                    }
-                }
-                catch (FileNotFoundException)
-                {
-                    // Means the file isn't cached yet
-                }
-                catch (IOException)
-                {
-                    // Means the file isn't cached yet
-                }
-
-                if (!hasFile)
-                {
-                    await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-                    contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
-                }
-
-                if (string.IsNullOrEmpty(contentPath))
+            try
+            {
+                contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+                if (System.IO.File.Exists(contentPath))
                 {
-                    return NotFound();
+                    hasFile = true;
                 }
+            }
+            catch (FileNotFoundException)
+            {
+                // Means the file isn't cached yet
+            }
+            catch (IOException)
+            {
+                // Means the file isn't cached yet
+            }
 
-                var contentType = MimeTypes.GetMimeType(contentPath);
-                return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType);
+            if (!hasFile)
+            {
+                await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
+                contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
             }
-            catch (Exception e)
+
+            if (string.IsNullOrEmpty(contentPath))
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            var contentType = MimeTypes.GetMimeType(contentPath);
+            return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType);
         }
 
         /// <summary>
@@ -227,24 +206,17 @@ namespace Jellyfin.Api.Controllers.Images
             [FromQuery, BindRequired] ImageType type,
             [FromQuery] string imageUrl)
         {
-            try
+            var item = _libraryManager.GetItemById(id);
+            if (item == null)
             {
-                var item = _libraryManager.GetItemById(id);
-                if (item == null)
-                {
-                    return NotFound();
-                }
+                return NotFound();
+            }
 
-                await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
-                    .ConfigureAwait(false);
+            await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
+                .ConfigureAwait(false);
 
-                item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-                return Ok();
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+            return Ok();
         }
 
         /// <summary>

From 8ab9949db5a1c0072ec35937cb96e93ce5b9d672 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 08:02:07 -0600
Subject: [PATCH 040/463] Remove exception handler

---
 .../Controllers/ScheduledTasksController.cs   | 145 +++++++-----------
 1 file changed, 56 insertions(+), 89 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 157e985197..acbc630c23 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -41,48 +41,41 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isHidden = false,
             [FromQuery] bool? isEnabled = false)
         {
-            try
-            {
-                IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
+            IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
 
-                if (isHidden.HasValue)
+            if (isHidden.HasValue)
+            {
+                var hiddenValue = isHidden.Value;
+                tasks = tasks.Where(o =>
                 {
-                    var hiddenValue = isHidden.Value;
-                    tasks = tasks.Where(o =>
+                    var itemIsHidden = false;
+                    if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
                     {
-                        var itemIsHidden = false;
-                        if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
-                        {
-                            itemIsHidden = configurableScheduledTask.IsHidden;
-                        }
+                        itemIsHidden = configurableScheduledTask.IsHidden;
+                    }
 
-                        return itemIsHidden == hiddenValue;
-                    });
-                }
+                    return itemIsHidden == hiddenValue;
+                });
+            }
 
-                if (isEnabled.HasValue)
+            if (isEnabled.HasValue)
+            {
+                var enabledValue = isEnabled.Value;
+                tasks = tasks.Where(o =>
                 {
-                    var enabledValue = isEnabled.Value;
-                    tasks = tasks.Where(o =>
+                    var itemIsEnabled = false;
+                    if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
                     {
-                        var itemIsEnabled = false;
-                        if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
-                        {
-                            itemIsEnabled = configurableScheduledTask.IsEnabled;
-                        }
+                        itemIsEnabled = configurableScheduledTask.IsEnabled;
+                    }
 
-                        return itemIsEnabled == enabledValue;
-                    });
-                }
+                    return itemIsEnabled == enabledValue;
+                });
+            }
 
-                var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo);
+            var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo);
 
-                return Ok(taskInfos);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
+            return Ok(taskInfos);
         }
 
         /// <summary>
@@ -96,23 +89,16 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult GetTask([FromRoute] string taskId)
         {
-            try
-            {
-                var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
-                    string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
-
-                if (task == null)
-                {
-                    return NotFound();
-                }
+            var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
+                string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
 
-                var result = ScheduledTaskHelpers.GetTaskInfo(task);
-                return Ok(result);
-            }
-            catch (Exception e)
+            if (task == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            var result = ScheduledTaskHelpers.GetTaskInfo(task);
+            return Ok(result);
         }
 
         /// <summary>
@@ -126,23 +112,16 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult StartTask([FromRoute] string taskId)
         {
-            try
-            {
-                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
-                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+            var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
 
-                if (task == null)
-                {
-                    return NotFound();
-                }
-
-                _taskManager.Execute(task, new TaskOptions());
-                return Ok();
-            }
-            catch (Exception e)
+            if (task == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            _taskManager.Execute(task, new TaskOptions());
+            return Ok();
         }
 
         /// <summary>
@@ -156,23 +135,16 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
         public IActionResult StopTask([FromRoute] string taskId)
         {
-            try
-            {
-                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
-                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
-
-                if (task == null)
-                {
-                    return NotFound();
-                }
+            var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
 
-                _taskManager.Cancel(task);
-                return Ok();
-            }
-            catch (Exception e)
+            if (task == null)
             {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            _taskManager.Cancel(task);
+            return Ok();
         }
 
         /// <summary>
@@ -185,24 +157,19 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult UpdateTask([FromRoute] string taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
+        public IActionResult UpdateTask(
+            [FromRoute] string taskId,
+            [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
         {
-            try
+            var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
+                o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
+            if (task == null)
             {
-                var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
-                    o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
-                if (task == null)
-                {
-                    return NotFound();
-                }
-
-                task.Triggers = triggerInfos;
-                return Ok();
-            }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+                return NotFound();
             }
+
+            task.Triggers = triggerInfos;
+            return Ok();
         }
     }
 }

From fe632146dcba69edeec56b850736227ff5f4c5b3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 08:17:13 -0600
Subject: [PATCH 041/463] Move Json Options to static class for easier access.

---
 .../CamelCaseJsonProfileFormatter.cs          |  4 +-
 .../PascalCaseJsonProfileFormatter.cs         |  4 +-
 Jellyfin.Server/Models/JsonOptions.cs         | 41 +++++++++++++++++++
 3 files changed, 45 insertions(+), 4 deletions(-)
 create mode 100644 Jellyfin.Server/Models/JsonOptions.cs

diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
index 433a3197d3..e6ad6dfb13 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -1,4 +1,4 @@
-using System.Text.Json;
+using Jellyfin.Server.Models;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
 
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public CamelCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
+        public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase)
         {
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
index 2ed006a336..675f4c79ee 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -1,4 +1,4 @@
-using System.Text.Json;
+using Jellyfin.Server.Models;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
 
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public PascalCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = null })
+        public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase)
         {
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs
new file mode 100644
index 0000000000..fa503bc9a4
--- /dev/null
+++ b/Jellyfin.Server/Models/JsonOptions.cs
@@ -0,0 +1,41 @@
+using System.Text.Json;
+
+namespace Jellyfin.Server.Models
+{
+    /// <summary>
+    /// Json Options.
+    /// </summary>
+    public static class JsonOptions
+    {
+        /// <summary>
+        /// Base Json Serializer Options.
+        /// </summary>
+        private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions();
+
+        /// <summary>
+        /// Gets CamelCase json options.
+        /// </summary>
+        public static JsonSerializerOptions CamelCase
+        {
+            get
+            {
+                var options = _jsonOptions;
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+                return options;
+            }
+        }
+
+        /// <summary>
+        /// Gets PascalCase json options.
+        /// </summary>
+        public static JsonSerializerOptions PascalCase
+        {
+            get
+            {
+                var options = _jsonOptions;
+                options.PropertyNamingPolicy = null;
+                return options;
+            }
+        }
+    }
+}

From 14361c68cf71bc810d282901a764d2f8d5858eea Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 08:38:31 -0600
Subject: [PATCH 042/463] Add ProducesResponseType to base controller

---
 Jellyfin.Api/BaseJellyfinApiController.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 1f4508e6cb..f691759866 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,3 +1,5 @@
+using Jellyfin.Api.Models.ExceptionDtos;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
@@ -7,6 +9,7 @@ namespace Jellyfin.Api
     /// </summary>
     [ApiController]
     [Route("[controller]")]
+    [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)]
     public class BaseJellyfinApiController : ControllerBase
     {
     }

From b8fd9c785e107b6d2ae8125d6e6b6374f36fe9a3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 08:42:48 -0600
Subject: [PATCH 043/463] Convert StartupController to IActionResult

---
 Jellyfin.Api/Controllers/StartupController.cs | 35 ++++++++++++-------
 1 file changed, 23 insertions(+), 12 deletions(-)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index afc9b8f3da..b0b26c1762 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -32,12 +33,15 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Api endpoint for completing the startup wizard.
         /// </summary>
+        /// <returns>Status.</returns>
         [HttpPost("Complete")]
-        public void CompleteWizard()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IActionResult CompleteWizard()
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.SetOptimalValues();
             _config.SaveConfiguration();
+            return Ok();
         }
 
         /// <summary>
@@ -45,7 +49,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>The initial startup wizard configuration.</returns>
         [HttpGet("Configuration")]
-        public StartupConfigurationDto GetStartupConfiguration()
+        [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)]
+        public IActionResult GetStartupConfiguration()
         {
             var result = new StartupConfigurationDto
             {
@@ -54,7 +59,7 @@ namespace Jellyfin.Api.Controllers
                 PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
             };
 
-            return result;
+            return Ok(result);
         }
 
         /// <summary>
@@ -63,8 +68,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="uiCulture">The UI language culture.</param>
         /// <param name="metadataCountryCode">The metadata country code.</param>
         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
+        /// <returns>Status.</returns>
         [HttpPost("Configuration")]
-        public void UpdateInitialConfiguration(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IActionResult UpdateInitialConfiguration(
             [FromForm] string uiCulture,
             [FromForm] string metadataCountryCode,
             [FromForm] string preferredMetadataLanguage)
@@ -73,6 +80,7 @@ namespace Jellyfin.Api.Controllers
             _config.Configuration.MetadataCountryCode = metadataCountryCode;
             _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
             _config.SaveConfiguration();
+            return Ok();
         }
 
         /// <summary>
@@ -80,12 +88,15 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="enableRemoteAccess">Enable remote access.</param>
         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
+        /// <returns>Status.</returns>
         [HttpPost("RemoteAccess")]
-        public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
         {
             _config.Configuration.EnableRemoteAccess = enableRemoteAccess;
             _config.Configuration.EnableUPnP = enableAutomaticPortMapping;
             _config.SaveConfiguration();
+            return Ok();
         }
 
         /// <summary>
@@ -93,14 +104,11 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
-        public StartupUserDto GetFirstUser()
+        [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)]
+        public IActionResult GetFirstUser()
         {
             var user = _userManager.Users.First();
-            return new StartupUserDto
-            {
-                Name = user.Name,
-                Password = user.Password
-            };
+            return Ok(new StartupUserDto { Name = user.Name, Password = user.Password });
         }
 
         /// <summary>
@@ -109,7 +117,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startupUserDto">The DTO containing username and password.</param>
         /// <returns>The async task.</returns>
         [HttpPost("User")]
-        public async Task UpdateUser([FromForm] StartupUserDto startupUserDto)
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<IActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
         {
             var user = _userManager.Users.First();
 
@@ -121,6 +130,8 @@ namespace Jellyfin.Api.Controllers
             {
                 await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
             }
+
+            return Ok();
         }
     }
 }

From 3ef8448a518e673feae0c70c2682d60e4632c0cd Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 09:09:05 -0600
Subject: [PATCH 044/463] Return to previous exception handle implementation

---
 Jellyfin.Api/BaseJellyfinApiController.cs     |  3 -
 .../Models/ExceptionDtos/ExceptionDto.cs      | 14 ---
 .../Middleware/ExceptionMiddleware.cs         | 86 ++++++++++++++++---
 3 files changed, 73 insertions(+), 30 deletions(-)
 delete mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs

diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index f691759866..1f4508e6cb 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,5 +1,3 @@
-using Jellyfin.Api.Models.ExceptionDtos;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
@@ -9,7 +7,6 @@ namespace Jellyfin.Api
     /// </summary>
     [ApiController]
     [Route("[controller]")]
-    [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)]
     public class BaseJellyfinApiController : ControllerBase
     {
     }
diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs
deleted file mode 100644
index d2b48d4ae5..0000000000
--- a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Jellyfin.Api.Models.ExceptionDtos
-{
-    /// <summary>
-    /// Exception Dto.
-    /// Used for graceful handling of API exceptions.
-    /// </summary>
-    public class ExceptionDto
-    {
-        /// <summary>
-        /// Gets or sets exception message.
-        /// </summary>
-        public string Message { get; set; }
-    }
-}
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 39aace95d2..0d9dac89f0 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -1,7 +1,9 @@
 using System;
-using System.Text.Json;
+using System.IO;
 using System.Threading.Tasks;
-using Jellyfin.Api.Models.ExceptionDtos;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -14,17 +16,22 @@ namespace Jellyfin.Server.Middleware
     {
         private readonly RequestDelegate _next;
         private readonly ILogger<ExceptionMiddleware> _logger;
+        private readonly IServerConfigurationManager _configuration;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
         /// </summary>
         /// <param name="next">Next request delegate.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
-        public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public ExceptionMiddleware(
+            RequestDelegate next,
+            ILoggerFactory loggerFactory,
+            IServerConfigurationManager serverConfigurationManager)
         {
-            _next = next ?? throw new ArgumentNullException(nameof(next));
-            _logger = loggerFactory.CreateLogger<ExceptionMiddleware>() ??
-                      throw new ArgumentNullException(nameof(loggerFactory));
+            _next = next;
+            _logger = loggerFactory.CreateLogger<ExceptionMiddleware>();
+            _configuration = serverConfigurationManager;
         }
 
         /// <summary>
@@ -46,15 +53,68 @@ namespace Jellyfin.Server.Middleware
                     throw;
                 }
 
-                var exceptionBody = new ExceptionDto { Message = ex.Message };
-                var exceptionJson = JsonSerializer.Serialize(exceptionBody);
+                ex = GetActualException(ex);
+                _logger.LogError(ex, "Error processing request: {0}", ex.Message);
+                context.Response.StatusCode = GetStatusCode(ex);
+                context.Response.ContentType = "text/plain";
 
-                context.Response.Clear();
-                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
-                // TODO switch between PascalCase and camelCase
-                context.Response.ContentType = "application/json";
-                await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false);
+                var errorContent = NormalizeExceptionMessage(ex.Message);
+                await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
             }
         }
+
+        private static Exception GetActualException(Exception ex)
+        {
+            if (ex is AggregateException agg)
+            {
+                var inner = agg.InnerException;
+                if (inner != null)
+                {
+                    return GetActualException(inner);
+                }
+
+                var inners = agg.InnerExceptions;
+                if (inners.Count > 0)
+                {
+                    return GetActualException(inners[0]);
+                }
+            }
+
+            return ex;
+        }
+
+        private static int GetStatusCode(Exception ex)
+        {
+            switch (ex)
+            {
+                case ArgumentException _: return StatusCodes.Status400BadRequest;
+                case SecurityException _: return StatusCodes.Status401Unauthorized;
+                case DirectoryNotFoundException _:
+                case FileNotFoundException _:
+                case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
+                case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
+                default: return StatusCodes.Status500InternalServerError;
+            }
+        }
+
+        private string NormalizeExceptionMessage(string msg)
+        {
+            if (msg == null)
+            {
+                return string.Empty;
+            }
+
+            // Strip any information we don't want to reveal
+            msg = msg.Replace(
+                _configuration.ApplicationPaths.ProgramSystemPath,
+                string.Empty,
+                StringComparison.OrdinalIgnoreCase);
+            msg = msg.Replace(
+                _configuration.ApplicationPaths.ProgramDataPath,
+                string.Empty,
+                StringComparison.OrdinalIgnoreCase);
+
+            return msg;
+        }
     }
 }

From 69d9bfb233bd2716e3803b38c55275de58bb8d46 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Tue, 21 Apr 2020 12:10:34 -0600
Subject: [PATCH 045/463] Make documentation of parameters clearer

Co-Authored-By: Vasily <JustAMan@users.noreply.github.com>
---
 Jellyfin.Api/Controllers/NotificationsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index c1d9e32515..6145246ed3 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Api.Controllers
         /// Endpoint for getting a user's notifications.
         /// </summary>
         /// <param name="userId">The user's ID.</param>
-        /// <param name="isRead">An optional filter by IsRead.</param>
+        /// <param name="isRead">An optional filter by notification read state.</param>
         /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>

From 466e20ea8cb8c262605d06dc01eff4463559d9b0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 13:57:11 -0600
Subject: [PATCH 046/463] move to ActionResult<T>

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index aeeaf5cbdc..351401de18 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -44,10 +44,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Attachment.</returns>
         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
         [Produces("application/octet-stream")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public async Task<IActionResult> GetAttachment(
+        public async Task<ActionResult<FileStreamResult>> GetAttachment(
             [FromRoute] Guid videoId,
             [FromRoute] string mediaSourceId,
             [FromRoute] int index)

From 927696c4036960018864864a4acbf0aeb797f7ac Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 13:59:43 -0600
Subject: [PATCH 047/463] move to ActionResult<T>

---
 Jellyfin.Api/Controllers/DevicesController.cs | 29 +++++++------------
 1 file changed, 11 insertions(+), 18 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 5dc3f27ee1..559a260071 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -49,9 +49,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Device Infos.</returns>
         [HttpGet]
         [Authenticated(Roles = "Admin")]
-        [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<DeviceInfo[]> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
             var devices = _deviceManager.GetDevices(deviceQuery);
@@ -65,10 +64,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Device Info.</returns>
         [HttpGet("Info")]
         [Authenticated(Roles = "Admin")]
-        [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id)
+        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string id)
         {
             var deviceInfo = _deviceManager.GetDevice(id);
             if (deviceInfo == null)
@@ -86,10 +84,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Device Info.</returns>
         [HttpGet("Options")]
         [Authenticated(Roles = "Admin")]
-        [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id)
+        public ActionResult<DeviceInfo> GetDeviceOptions([FromQuery, BindRequired] string id)
         {
             var deviceInfo = _deviceManager.GetDeviceOptions(id);
             if (deviceInfo == null)
@@ -110,8 +107,7 @@ namespace Jellyfin.Api.Controllers
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult UpdateDeviceOptions(
+        public ActionResult UpdateDeviceOptions(
             [FromQuery, BindRequired] string id,
             [FromBody, BindRequired] DeviceOptions deviceOptions)
         {
@@ -132,8 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult DeleteDevice([FromQuery, BindRequired] string id)
+        public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
         {
             var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
 
@@ -151,9 +146,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">Device Id.</param>
         /// <returns>Content Upload History.</returns>
         [HttpGet("CameraUploads")]
-        [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetCameraUploads([FromQuery, BindRequired] string id)
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ContentUploadHistory> GetCameraUploads([FromQuery, BindRequired] string id)
         {
             var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
             return Ok(uploadHistory);
@@ -170,8 +164,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("CameraUploads")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public async Task<IActionResult> PostCameraUploadAsync(
+        public async Task<ActionResult> PostCameraUploadAsync(
             [FromQuery, BindRequired] string deviceId,
             [FromQuery, BindRequired] string album,
             [FromQuery, BindRequired] string name,

From 98224dee9e3bfc2bb30c14792aec4bda47670863 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 14:01:47 -0600
Subject: [PATCH 048/463] move to ActionResult<T>

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index e15e9c4be6..25391bcf84 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -36,10 +36,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="client">Client.</param>
         /// <returns>Display Preferences.</returns>
         [HttpGet("{DisplayPreferencesId}")]
-        [ProducesResponseType(typeof(DisplayPreferences), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
-        public IActionResult GetDisplayPreferences(
+        public ActionResult<DisplayPreferences> GetDisplayPreferences(
             [FromRoute] string displayPreferencesId,
             [FromQuery] [Required] string userId,
             [FromQuery] [Required] string client)
@@ -65,8 +64,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult UpdateDisplayPreferences(
+        public ActionResult UpdateDisplayPreferences(
             [FromRoute] string displayPreferencesId,
             [FromQuery, BindRequired] string userId,
             [FromQuery, BindRequired] string client,

From 02a78aaae98bdecacd04325e124bde9224c66955 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 14:07:11 -0600
Subject: [PATCH 049/463] move to ActionResult<T>

---
 .../Images/ImageByNameController.cs           | 35 +++++++++----------
 1 file changed, 16 insertions(+), 19 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index 4034c9e857..ce509b4e6d 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -44,9 +44,8 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <returns>General images.</returns>
         [HttpGet("General")]
-        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetGeneralImages()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ImageByNameInfo[]> GetGeneralImages()
         {
             return Ok(GetImageList(_applicationPaths.GeneralPath, false));
         }
@@ -58,10 +57,10 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
         /// <returns>Image Stream.</returns>
         [HttpGet("General/{Name}/{Type}")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type)
+        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type)
         {
             var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
                 ? "folder"
@@ -85,9 +84,8 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <returns>General images.</returns>
         [HttpGet("Ratings")]
-        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetRatingImages()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ImageByNameInfo[]> GetRatingImages()
         {
             return Ok(GetImageList(_applicationPaths.RatingsPath, false));
         }
@@ -99,10 +97,10 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="name">The name of the image.</param>
         /// <returns>Image Stream.</returns>
         [HttpGet("Ratings/{Theme}/{Name}")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetRatingImage(
+        public ActionResult<FileStreamResult> GetRatingImage(
             [FromRoute] string theme,
             [FromRoute] string name)
         {
@@ -114,9 +112,8 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <returns>Media Info images.</returns>
         [HttpGet("MediaInfo")]
-        [ProducesResponseType(typeof(ImageByNameInfo[]), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetMediaInfoImages()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ImageByNameInfo[]> GetMediaInfoImages()
         {
             return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
         }
@@ -128,10 +125,10 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="name">The name of the image.</param>
         /// <returns>Image Stream.</returns>
         [HttpGet("MediaInfo/{Theme}/{Name}")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetMediaInfoImage(
+        public ActionResult<FileStreamResult> GetMediaInfoImage(
             [FromRoute] string theme,
             [FromRoute] string name)
         {
@@ -145,7 +142,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="theme">Theme to search.</param>
         /// <param name="name">File name to search for.</param>
         /// <returns>Image Stream.</returns>
-        private IActionResult GetImageFile(string basePath, string theme, string name)
+        private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name)
         {
             var themeFolder = Path.Combine(basePath, theme);
             if (Directory.Exists(themeFolder))

From 9ae895ba213a508f676d21e5425b25bb518ed89a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 14:09:06 -0600
Subject: [PATCH 050/463] move to ActionResult<T>

---
 .../Images/RemoteImageController.cs           | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index 8c7d21cd53..a0754ed4eb 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -63,10 +63,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="includeAllLanguages">Optinal. Include all languages.</param>
         /// <returns>Remote Image Result.</returns>
         [HttpGet("{Id}/RemoteImages")]
-        [ProducesResponseType(typeof(RemoteImageResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
-        public async Task<IActionResult> GetRemoteImages(
+        public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
             [FromRoute] string id,
             [FromQuery] ImageType? type,
             [FromQuery] int? startIndex,
@@ -126,10 +125,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="id">Item Id.</param>
         /// <returns>List of providers.</returns>
         [HttpGet("{Id}/RemoteImages/Providers")]
-        [ProducesResponseType(typeof(ImageProviderInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetRemoteImageProviders([FromRoute] string id)
+        public ActionResult<ImageProviderInfo[]> GetRemoteImageProviders([FromRoute] string id)
         {
             var item = _libraryManager.GetItemById(id);
             if (item == null)
@@ -147,10 +145,10 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="imageUrl">The image url.</param>
         /// <returns>Image Stream.</returns>
         [HttpGet("Remote")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public async Task<IActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
+        public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
         {
             var urlHash = imageUrl.GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
@@ -200,8 +198,7 @@ namespace Jellyfin.Api.Controllers.Images
         [HttpPost("{Id}/RemoteImages/Download")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public async Task<IActionResult> DownloadRemoteImage(
+        public async Task<ActionResult> DownloadRemoteImage(
             [FromRoute] string id,
             [FromQuery, BindRequired] ImageType type,
             [FromQuery] string imageUrl)

From 88b856796a9e4852ae4f9938baddd4741e8285d5 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 14:23:08 -0600
Subject: [PATCH 051/463] move to ActionResult<T>

---
 .../Controllers/ScheduledTasksController.cs   | 53 ++++++-------------
 1 file changed, 16 insertions(+), 37 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index acbc630c23..da7cfbc3a7 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -14,7 +14,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Scheduled Tasks Controller.
     /// </summary>
-    [Authenticated]
+    // [Authenticated]
     public class ScheduledTasksController : BaseJellyfinApiController
     {
         private readonly ITaskManager _taskManager;
@@ -35,47 +35,30 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
         /// <returns>Task list.</returns>
         [HttpGet]
-        [ProducesResponseType(typeof(TaskInfo[]), StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetTasks(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<IScheduledTaskWorker> GetTasks(
             [FromQuery] bool? isHidden = false,
             [FromQuery] bool? isEnabled = false)
         {
             IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
 
-            if (isHidden.HasValue)
+            foreach (var task in tasks)
             {
-                var hiddenValue = isHidden.Value;
-                tasks = tasks.Where(o =>
+                if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
                 {
-                    var itemIsHidden = false;
-                    if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
+                    if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
                     {
-                        itemIsHidden = configurableScheduledTask.IsHidden;
+                        continue;
                     }
 
-                    return itemIsHidden == hiddenValue;
-                });
-            }
-
-            if (isEnabled.HasValue)
-            {
-                var enabledValue = isEnabled.Value;
-                tasks = tasks.Where(o =>
-                {
-                    var itemIsEnabled = false;
-                    if (o.ScheduledTask is IConfigurableScheduledTask configurableScheduledTask)
+                    if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
                     {
-                        itemIsEnabled = configurableScheduledTask.IsEnabled;
+                        continue;
                     }
+                }
 
-                    return itemIsEnabled == enabledValue;
-                });
+                yield return task;
             }
-
-            var taskInfos = tasks.Select(ScheduledTaskHelpers.GetTaskInfo);
-
-            return Ok(taskInfos);
         }
 
         /// <summary>
@@ -84,10 +67,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="taskId">Task Id.</param>
         /// <returns>Task Info.</returns>
         [HttpGet("{TaskID}")]
-        [ProducesResponseType(typeof(TaskInfo), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult GetTask([FromRoute] string taskId)
+        public ActionResult<TaskInfo> GetTask([FromRoute] string taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
                 string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -109,8 +91,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Running/{TaskID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult StartTask([FromRoute] string taskId)
+        public ActionResult StartTask([FromRoute] string taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -132,8 +113,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Running/{TaskID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult StopTask([FromRoute] string taskId)
+        public ActionResult StopTask([FromRoute] string taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -156,8 +136,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("{TaskID}/Triggers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public IActionResult UpdateTask(
+        public ActionResult UpdateTask(
             [FromRoute] string taskId,
             [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
         {

From 7db3b035a6d1f7e6f4886c4497b98b7a6af6c679 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 14:25:03 -0600
Subject: [PATCH 052/463] move to ActionResult<T>

---
 Jellyfin.Api/Controllers/StartupController.cs | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index b0b26c1762..2db7e32aad 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpPost("Complete")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult CompleteWizard()
+        public ActionResult CompleteWizard()
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.SetOptimalValues();
@@ -49,8 +49,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>The initial startup wizard configuration.</returns>
         [HttpGet("Configuration")]
-        [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)]
-        public IActionResult GetStartupConfiguration()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
         {
             var result = new StartupConfigurationDto
             {
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpPost("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult UpdateInitialConfiguration(
+        public ActionResult UpdateInitialConfiguration(
             [FromForm] string uiCulture,
             [FromForm] string metadataCountryCode,
             [FromForm] string preferredMetadataLanguage)
@@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Status.</returns>
         [HttpPost("RemoteAccess")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
+        public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
         {
             _config.Configuration.EnableRemoteAccess = enableRemoteAccess;
             _config.Configuration.EnableUPnP = enableAutomaticPortMapping;
@@ -104,8 +104,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
-        [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)]
-        public IActionResult GetFirstUser()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<StartupUserDto> GetFirstUser()
         {
             var user = _userManager.Users.First();
             return Ok(new StartupUserDto { Name = user.Name, Password = user.Password });
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The async task.</returns>
         [HttpPost("User")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<IActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
+        public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
         {
             var user = _userManager.Users.First();
 

From 3ab61dbdc252670abf28797d3183614b1cd05ece Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 15:49:04 -0600
Subject: [PATCH 053/463] bump swashbuckle

---
 Jellyfin.Api/Jellyfin.Api.csproj | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index cbb1d3007f..77bb52c6a5 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -10,8 +10,8 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.3" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.3.2" />
-    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.2" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.3.3" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
   </ItemGroup>
 
   <ItemGroup>

From 2542a27bd5f79ccfbc2547ddd877ddb0423ae296 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 16:15:31 -0600
Subject: [PATCH 054/463] Fix documentation endpoint for use with baseUrl

---
 .../ApiApplicationBuilderExtensions.cs        | 28 ++++++++++++++-----
 Jellyfin.Server/Startup.cs                    |  2 +-
 2 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 43c49307d4..df3bab931b 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
 
 namespace Jellyfin.Server.Extensions
@@ -11,23 +12,36 @@ namespace Jellyfin.Server.Extensions
         /// Adds swagger and swagger UI to the application pipeline.
         /// </summary>
         /// <param name="applicationBuilder">The application builder.</param>
+        /// <param name="serverConfigurationManager">The server configuration.</param>
         /// <returns>The updated application builder.</returns>
-        public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder)
+        public static IApplicationBuilder UseJellyfinApiSwagger(
+            this IApplicationBuilder applicationBuilder,
+            IServerConfigurationManager serverConfigurationManager)
         {
             // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
             // specifying the Swagger JSON endpoint.
-            const string specEndpoint = "/swagger/v1/swagger.json";
+
+            var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/');
+            if (!string.IsNullOrEmpty(baseUrl))
+            {
+                baseUrl += '/';
+            }
+
             return applicationBuilder
-                .UseSwagger()
+                .UseSwagger(c =>
+                {
+                    c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json";
+                })
                 .UseSwaggerUI(c =>
                 {
-                    c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1");
-                    c.RoutePrefix = "api-docs/swagger";
+                    c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1");
+                    c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger";
                 })
                 .UseReDoc(c =>
                 {
-                    c.SpecUrl(specEndpoint);
-                    c.RoutePrefix = "api-docs/redoc";
+                    c.DocumentTitle = "Jellyfin API v1";
+                    c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json");
+                    c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc";
                 });
         }
     }
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 4d7d56e9d4..ee08d2580a 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Server
             app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
 
             // TODO use when old API is removed: app.UseAuthentication();
-            app.UseJellyfinApiSwagger();
+            app.UseJellyfinApiSwagger(_serverConfigurationManager);
             app.UseRouting();
             app.UseAuthorization();
             app.UseEndpoints(endpoints =>

From 041d674eb6e4a675b68406ed5c2d7018d61e870a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 16:19:26 -0600
Subject: [PATCH 055/463] Fix swagger ui Document Title

---
 Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index df3bab931b..d094242259 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -34,6 +34,7 @@ namespace Jellyfin.Server.Extensions
                 })
                 .UseSwaggerUI(c =>
                 {
+                    c.DocumentTitle = "Jellyfin API v1";
                     c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1");
                     c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger";
                 })

From f5385e4735849cbb1552e69faa0116e5498b3688 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 18:12:46 -0600
Subject: [PATCH 056/463] Move Emby.Dlna DlnaService.cs to Jellyfin.Api

---
 Emby.Dlna/Api/DlnaService.cs               |  88 ---------------
 Jellyfin.Api/Controllers/DlnaController.cs | 124 +++++++++++++++++++++
 2 files changed, 124 insertions(+), 88 deletions(-)
 delete mode 100644 Emby.Dlna/Api/DlnaService.cs
 create mode 100644 Jellyfin.Api/Controllers/DlnaController.cs

diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs
deleted file mode 100644
index 5f984bb335..0000000000
--- a/Emby.Dlna/Api/DlnaService.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
-    [Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
-    public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
-    {
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
-    public class DeleteProfile : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
-    public class GetDefaultProfile : IReturn<DeviceProfile>
-    {
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
-    public class GetProfile : IReturn<DeviceProfile>
-    {
-        [ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
-    public class UpdateProfile : DeviceProfile, IReturnVoid
-    {
-    }
-
-    [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
-    public class CreateProfile : DeviceProfile, IReturnVoid
-    {
-    }
-
-    [Authenticated(Roles = "Admin")]
-    public class DlnaService : IService
-    {
-        private readonly IDlnaManager _dlnaManager;
-
-        public DlnaService(IDlnaManager dlnaManager)
-        {
-            _dlnaManager = dlnaManager;
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetProfileInfos request)
-        {
-            return _dlnaManager.GetProfileInfos().ToArray();
-        }
-
-        public object Get(GetProfile request)
-        {
-            return _dlnaManager.GetProfile(request.Id);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetDefaultProfile request)
-        {
-            return _dlnaManager.GetDefaultProfile();
-        }
-
-        public void Delete(DeleteProfile request)
-        {
-            _dlnaManager.DeleteProfile(request.Id);
-        }
-
-        public void Post(UpdateProfile request)
-        {
-            _dlnaManager.UpdateProfile(request);
-        }
-
-        public void Post(CreateProfile request)
-        {
-            _dlnaManager.CreateProfile(request);
-        }
-    }
-}
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
new file mode 100644
index 0000000000..68cd144f4e
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -0,0 +1,124 @@
+#nullable enable
+
+using System.Collections.Generic;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Dlna Controller.
+    /// </summary>
+    [Authenticated(Roles = "Admin")]
+    public class DlnaController : BaseJellyfinApiController
+    {
+        private readonly IDlnaManager _dlnaManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DlnaController"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        public DlnaController(IDlnaManager dlnaManager)
+        {
+            _dlnaManager = dlnaManager;
+        }
+
+        /// <summary>
+        /// Get profile infos.
+        /// </summary>
+        /// <returns>Profile infos.</returns>
+        [HttpGet("ProfileInfos")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<DeviceProfileInfo> GetProfileInfos()
+        {
+            return _dlnaManager.GetProfileInfos();
+        }
+
+        /// <summary>
+        /// Gets the default profile.
+        /// </summary>
+        /// <returns>Default profile.</returns>
+        [HttpGet("Profiles/Default")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<DeviceProfileInfo> GetDefaultProfile()
+        {
+            return Ok(_dlnaManager.GetDefaultProfile());
+        }
+
+        /// <summary>
+        /// Gets a single profile.
+        /// </summary>
+        /// <param name="id">Profile Id.</param>
+        /// <returns>Profile.</returns>
+        [HttpGet("Profiles/{Id}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<DeviceProfileInfo> GetProfile([FromRoute] string id)
+        {
+            var profile = _dlnaManager.GetProfile(id);
+            if (profile == null)
+            {
+                return NotFound();
+            }
+
+            return Ok(profile);
+        }
+
+        /// <summary>
+        /// Deletes a profile.
+        /// </summary>
+        /// <param name="id">Profile id.</param>
+        /// <returns>Status.</returns>
+        [HttpDelete("Profiles/{Id}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult DeleteProfile([FromRoute] string id)
+        {
+            var existingDeviceProfile = _dlnaManager.GetProfile(id);
+            if (existingDeviceProfile == null)
+            {
+                return NotFound();
+            }
+
+            _dlnaManager.DeleteProfile(id);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Creates a profile.
+        /// </summary>
+        /// <param name="deviceProfile">Device profile.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Profiles")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
+        {
+            _dlnaManager.CreateProfile(deviceProfile);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Updates a profile.
+        /// </summary>
+        /// <param name="id">Profile id.</param>
+        /// <param name="deviceProfile">Device profile.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Profiles/{Id}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult UpdateProfile([FromRoute] string id, [FromBody] DeviceProfile deviceProfile)
+        {
+            var existingDeviceProfile = _dlnaManager.GetProfile(id);
+            if (existingDeviceProfile == null)
+            {
+                return NotFound();
+            }
+
+            _dlnaManager.UpdateProfile(deviceProfile);
+            return Ok();
+        }
+    }
+}

From 461b298be7247afd7f7962604efab3b58b9dae4b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 19:15:27 -0600
Subject: [PATCH 057/463] Migrate DlnaServerController to Jellyfin.Api

---
 Emby.Dlna/Api/DlnaServerService.cs            | 383 ------------------
 Emby.Dlna/Main/DlnaEntryPoint.cs              |   6 +-
 .../Attributes/HttpSubscribeAttribute.cs      |  35 ++
 .../Attributes/HttpUnsubscribeAttribute.cs    |  35 ++
 .../Controllers/DlnaServerController.cs       | 259 ++++++++++++
 Jellyfin.Api/Jellyfin.Api.csproj              |   1 +
 6 files changed, 333 insertions(+), 386 deletions(-)
 delete mode 100644 Emby.Dlna/Api/DlnaServerService.cs
 create mode 100644 Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
 create mode 100644 Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
 create mode 100644 Jellyfin.Api/Controllers/DlnaServerController.cs

diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs
deleted file mode 100644
index 7fba2184a7..0000000000
--- a/Emby.Dlna/Api/DlnaServerService.cs
+++ /dev/null
@@ -1,383 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using Emby.Dlna.Main;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Dlna.Api
-{
-    [Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
-    [Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
-    public class GetDescriptionXml
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
-    [Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
-    public class GetContentDirectory
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
-    [Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
-    public class GetConnnectionManager
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
-    public class GetMediaReceiverRegistrar
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
-    public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
-    public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
-    public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UuId { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessMediaReceiverRegistrarEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessContentDirectoryEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
-    [Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
-    public class ProcessConnectionManagerEventRequest
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
-        public string UuId { get; set; }
-    }
-
-    [Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
-    [Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
-    public class GetIcon
-    {
-        [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UuId { get; set; }
-
-        [ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Filename { get; set; }
-    }
-
-    public class DlnaServerService : IService, IRequiresRequest
-    {
-        private const string XMLContentType = "text/xml; charset=UTF-8";
-
-        private readonly IDlnaManager _dlnaManager;
-        private readonly IHttpResultFactory _resultFactory;
-        private readonly IServerConfigurationManager _configurationManager;
-
-        public IRequest Request { get; set; }
-
-        private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
-
-        private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
-
-        private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
-
-        public DlnaServerService(
-            IDlnaManager dlnaManager,
-            IHttpResultFactory httpResultFactory,
-            IServerConfigurationManager configurationManager)
-        {
-            _dlnaManager = dlnaManager;
-            _resultFactory = httpResultFactory;
-            _configurationManager = configurationManager;
-        }
-
-        private string GetHeader(string name)
-        {
-            return Request.Headers[name];
-        }
-
-        public object Get(GetDescriptionXml request)
-        {
-            var url = Request.AbsoluteUri;
-            var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
-            var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
-
-            var cacheLength = TimeSpan.FromDays(1);
-            var cacheKey = Request.RawUrl.GetMD5();
-            var bytes = Encoding.UTF8.GetBytes(xml);
-
-            return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetContentDirectory request)
-        {
-            var xml = ContentDirectory.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetMediaReceiverRegistrar request)
-        {
-            var xml = MediaReceiverRegistrar.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetConnnectionManager request)
-        {
-            var xml = ConnectionManager.GetServiceXml();
-
-            return _resultFactory.GetResult(Request, xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessContentDirectoryControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        public async Task<object> Post(ProcessConnectionManagerControlRequest request)
-        {
-            var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
-
-            return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
-        }
-
-        private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
-        {
-            var id = GetPathValue(2).ToString();
-
-            return service.ProcessControlRequestAsync(new ControlRequest
-            {
-                Headers = Request.Headers,
-                InputXml = requestStream,
-                TargetServerUuId = id,
-                RequestedUrl = Request.AbsoluteUri
-            });
-        }
-
-        // Copied from MediaBrowser.Api/BaseApiService.cs
-        // TODO: Remove code duplication
-        /// <summary>
-        /// Gets the path segment at the specified index.
-        /// </summary>
-        /// <param name="index">The index of the path segment.</param>
-        /// <returns>The path segment at the specified index.</returns>
-        /// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
-        /// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
-        protected internal ReadOnlySpan<char> GetPathValue(int index)
-        {
-            static void ThrowIndexOutOfRangeException()
-                => throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
-
-            static void ThrowInvalidDataException()
-                => throw new InvalidDataException("Path doesn't start with the base url.");
-
-            ReadOnlySpan<char> path = Request.PathInfo;
-
-            // Remove the protocol part from the url
-            int pos = path.LastIndexOf("://");
-            if (pos != -1)
-            {
-                path = path.Slice(pos + 3);
-            }
-
-            // Remove the query string
-            pos = path.LastIndexOf('?');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            // Remove the domain
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(pos);
-            }
-
-            // Remove base url
-            string baseUrl = _configurationManager.Configuration.BaseUrl;
-            int baseUrlLen = baseUrl.Length;
-            if (baseUrlLen != 0)
-            {
-                if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
-                {
-                    path = path.Slice(baseUrlLen);
-                }
-                else
-                {
-                    // The path doesn't start with the base url,
-                    // how did we get here?
-                    ThrowInvalidDataException();
-                }
-            }
-
-            // Remove leading /
-            path = path.Slice(1);
-
-            // Backwards compatibility
-            const string Emby = "emby/";
-            if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(Emby.Length);
-            }
-
-            const string MediaBrowser = "mediabrowser/";
-            if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(MediaBrowser.Length);
-            }
-
-            // Skip segments until we are at the right index
-            for (int i = 0; i < index; i++)
-            {
-                pos = path.IndexOf('/');
-                if (pos == -1)
-                {
-                    ThrowIndexOutOfRangeException();
-                }
-
-                path = path.Slice(pos + 1);
-            }
-
-            // Remove the rest
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            return path;
-        }
-
-        public object Get(GetIcon request)
-        {
-            var contentType = "image/" + Path.GetExtension(request.Filename)
-                                            .TrimStart('.')
-                                            .ToLowerInvariant();
-
-            var cacheLength = TimeSpan.FromDays(365);
-            var cacheKey = Request.RawUrl.GetMD5();
-
-            return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessContentDirectoryEventRequest request)
-        {
-            return ProcessEventRequest(ContentDirectory);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessConnectionManagerEventRequest request)
-        {
-            return ProcessEventRequest(ConnectionManager);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
-        {
-            return ProcessEventRequest(MediaReceiverRegistrar);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessContentDirectoryEventRequest request)
-        {
-            return ProcessEventRequest(ContentDirectory);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessConnectionManagerEventRequest request)
-        {
-            return ProcessEventRequest(ConnectionManager);
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
-        {
-            return ProcessEventRequest(MediaReceiverRegistrar);
-        }
-
-        private object ProcessEventRequest(IEventManager eventManager)
-        {
-            var subscriptionId = GetHeader("SID");
-
-            if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
-            {
-                var notificationType = GetHeader("NT");
-
-                var callback = GetHeader("CALLBACK");
-                var timeoutString = GetHeader("TIMEOUT");
-
-                if (string.IsNullOrEmpty(notificationType))
-                {
-                    return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
-                }
-
-                return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
-            }
-
-            return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
-        }
-
-        private object GetSubscriptionResponse(EventSubscriptionResponse response)
-        {
-            return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
-        }
-    }
-}
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index c5d60b2a05..c0d01f4480 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -57,11 +57,11 @@ namespace Emby.Dlna.Main
 
         private ISsdpCommunicationsServer _communicationsServer;
 
-        internal IContentDirectory ContentDirectory { get; private set; }
+        public IContentDirectory ContentDirectory { get; private set; }
 
-        internal IConnectionManager ConnectionManager { get; private set; }
+        public IConnectionManager ConnectionManager { get; private set; }
 
-        internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
+        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
 
         public static DlnaEntryPoint Current;
 
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
new file mode 100644
index 0000000000..2fdd1e4899
--- /dev/null
+++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Identifies an action that supports the HTTP GET method.
+    /// </summary>
+    public class HttpSubscribeAttribute : HttpMethodAttribute
+    {
+        private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
+        /// </summary>
+        public HttpSubscribeAttribute()
+            : base(_supportedMethods)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
+        /// </summary>
+        /// <param name="template">The route template. May not be null.</param>
+        public HttpSubscribeAttribute(string template)
+            : base(_supportedMethods, template)
+        {
+            if (template == null)
+            {
+                throw new ArgumentNullException(nameof(template));
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
new file mode 100644
index 0000000000..d6d7e4563d
--- /dev/null
+++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Identifies an action that supports the HTTP GET method.
+    /// </summary>
+    public class HttpUnsubscribeAttribute : HttpMethodAttribute
+    {
+        private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
+        /// </summary>
+        public HttpUnsubscribeAttribute()
+            : base(_supportedMethods)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
+        /// </summary>
+        /// <param name="template">The route template. May not be null.</param>
+        public HttpUnsubscribeAttribute(string template)
+            : base(_supportedMethods, template)
+        {
+            if (template == null)
+            {
+                throw new ArgumentNullException(nameof(template));
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
new file mode 100644
index 0000000000..731d6707cf
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -0,0 +1,259 @@
+#nullable enable
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Emby.Dlna;
+using Emby.Dlna.Main;
+using Jellyfin.Api.Attributes;
+using MediaBrowser.Controller.Dlna;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+#pragma warning disable CA1801
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Dlna Server Controller.
+    /// </summary>
+    [Route("Dlna")]
+    public class DlnaServerController : BaseJellyfinApiController
+    {
+        private const string XMLContentType = "text/xml; charset=UTF-8";
+
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IContentDirectory _contentDirectory;
+        private readonly IConnectionManager _connectionManager;
+        private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        public DlnaServerController(IDlnaManager dlnaManager)
+        {
+            _dlnaManager = dlnaManager;
+            _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
+            _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
+            _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+        }
+
+        /// <summary>
+        /// Get Description Xml.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Description Xml.</returns>
+        [HttpGet("{Uuid}/description.xml")]
+        [HttpGet("{Uuid}/description")]
+        [Produces(XMLContentType)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetDescriptionXml([FromRoute] string uuid)
+        {
+            var url = GetAbsoluteUri();
+            var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
+            var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, uuid, serverAddress);
+
+            // TODO GetStaticResult doesn't do anything special?
+            /*
+            var cacheLength = TimeSpan.FromDays(1);
+            var cacheKey = Request.Path.Value.GetMD5();
+            var bytes = Encoding.UTF8.GetBytes(xml);
+            */
+            return Ok(xml);
+        }
+
+        /// <summary>
+        /// Gets Dlna content directory xml.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Dlna content directory xml.</returns>
+        [HttpGet("{Uuid}/ContentDirectory/ContentDirectory.xml")]
+        [HttpGet("{Uuid}/ContentDirectory/ContentDirectory")]
+        [Produces(XMLContentType)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetContentDirectory([FromRoute] string uuid)
+        {
+            return Ok(_contentDirectory.GetServiceXml());
+        }
+
+        /// <summary>
+        /// Gets Dlna media receiver registrar xml.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Dlna media receiver registrar xml.</returns>
+        [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
+        [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
+        [Produces(XMLContentType)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetMediaReceiverRegistrar([FromRoute] string uuid)
+        {
+            return Ok(_mediaReceiverRegistrar.GetServiceXml());
+        }
+
+        /// <summary>
+        /// Gets Dlna media receiver registrar xml.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Dlna media receiver registrar xml.</returns>
+        [HttpGet("{Uuid}/ConnectionManager/ConnectionManager.xml")]
+        [HttpGet("{Uuid}/ConnectionManager/ConnectionManager")]
+        [Produces(XMLContentType)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetConnectionManager([FromRoute] string uuid)
+        {
+            return Ok(_connectionManager.GetServiceXml());
+        }
+
+        /// <summary>
+        /// Process a content directory control request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Control response.</returns>
+        [HttpPost("{Uuid}/ContentDirectory/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string uuid)
+        {
+            var response = await PostAsync(uuid, Request.Body, _contentDirectory).ConfigureAwait(false);
+            return Ok(response);
+        }
+
+        /// <summary>
+        /// Process a connection manager control request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Control response.</returns>
+        [HttpPost("{Uuid}/ConnectionManager/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string uuid)
+        {
+            var response = await PostAsync(uuid, Request.Body, _connectionManager).ConfigureAwait(false);
+            return Ok(response);
+        }
+
+        /// <summary>
+        /// Process a media receiver registrar control request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Control response.</returns>
+        [HttpPost("{Uuid}/MediaReceiverRegistrar/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string uuid)
+        {
+            var response = await PostAsync(uuid, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
+            return Ok(response);
+        }
+
+        /// <summary>
+        /// Processes an event subscription request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Event subscription response.</returns>
+        [HttpSubscribe("{Uuid}/MediaReceiverRegistrar/Events")]
+        [HttpUnsubscribe("{Uuid}/MediaReceiverRegistrar/Events")]
+        public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string uuid)
+        {
+            return Ok(ProcessEventRequest(_mediaReceiverRegistrar));
+        }
+
+        /// <summary>
+        /// Processes an event subscription request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Event subscription response.</returns>
+        [HttpSubscribe("{Uuid}/ContentDirectory/Events")]
+        [HttpUnsubscribe("{Uuid}/ContentDirectory/Events")]
+        public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string uuid)
+        {
+            return Ok(ProcessEventRequest(_contentDirectory));
+        }
+
+        /// <summary>
+        /// Processes an event subscription request.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <returns>Event subscription response.</returns>
+        [HttpSubscribe("{Uuid}/ConnectionManager/Events")]
+        [HttpUnsubscribe("{Uuid}/ConnectionManager/Events")]
+        public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string uuid)
+        {
+            return Ok(ProcessEventRequest(_connectionManager));
+        }
+
+        /// <summary>
+        /// Gets a server icon.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <param name="fileName">The icon filename.</param>
+        /// <returns>Icon stream.</returns>
+        [HttpGet("{Uuid}/icons/{Filename}")]
+        public ActionResult<FileStreamResult> GetIconId([FromRoute] string uuid, [FromRoute] string fileName)
+        {
+            return GetIcon(fileName);
+        }
+
+        /// <summary>
+        /// Gets a server icon.
+        /// </summary>
+        /// <param name="uuid">Server UUID.</param>
+        /// <param name="fileName">The icon filename.</param>
+        /// <returns>Icon stream.</returns>
+        [HttpGet("icons/{Filename}")]
+        public ActionResult<FileStreamResult> GetIcon([FromQuery] string uuid, [FromRoute] string fileName)
+        {
+            return GetIcon(fileName);
+        }
+
+        private ActionResult<FileStreamResult> GetIcon(string fileName)
+        {
+            var icon = _dlnaManager.GetIcon(fileName);
+            if (icon == null)
+            {
+                return NotFound();
+            }
+
+            var contentType = "image/" + Path.GetExtension(fileName)
+                .TrimStart('.')
+                .ToLowerInvariant();
+
+            return new FileStreamResult(icon.Stream, contentType);
+        }
+
+        private string GetAbsoluteUri()
+        {
+            return $"{Request.Scheme}://{Request.Host}{Request.Path}";
+        }
+
+        private Task<ControlResponse> PostAsync(string id, Stream requestStream, IUpnpService service)
+        {
+            return service.ProcessControlRequestAsync(new ControlRequest
+            {
+                Headers = Request.Headers,
+                InputXml = requestStream,
+                TargetServerUuId = id,
+                RequestedUrl = GetAbsoluteUri()
+            });
+        }
+
+        private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
+        {
+            var subscriptionId = Request.Headers["SID"];
+            if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
+            {
+                var notificationType = Request.Headers["NT"];
+                var callback = Request.Headers["CALLBACK"];
+                var timeoutString = Request.Headers["TIMEOUT"];
+
+                if (string.IsNullOrEmpty(notificationType))
+                {
+                    return eventManager.RenewEventSubscription(
+                        subscriptionId,
+                        notificationType,
+                        timeoutString,
+                        callback);
+                }
+
+                return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
+            }
+
+            return eventManager.CancelEventSubscription(subscriptionId);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 8f23ef9d03..a2e116fd7c 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,6 +14,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
   </ItemGroup>
 

From 2a49b19a7c02f16cd4bb1d847c1ff76c5df316fb Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 22 Apr 2020 00:21:37 -0600
Subject: [PATCH 058/463] Update documentation of startIndex

Co-Authored-By: Vasily <JustAMan@users.noreply.github.com>
---
 Jellyfin.Api/Controllers/NotificationsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 6145246ed3..bb9f5a7b3c 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The user's ID.</param>
         /// <param name="isRead">An optional filter by notification read state.</param>
-        /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be dropped from the results.</param>
+        /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be omitted from the results.</param>
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]

From 7693cc0db006ef4eb3a90d161b14ac4551bb96a7 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 22 Apr 2020 10:00:10 -0600
Subject: [PATCH 059/463] Use ActionResult return type for all endpoints

---
 .../Controllers/NotificationsController.cs    | 28 +++++++++++--------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index bb9f5a7b3c..0bf3aa1b47 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -43,8 +43,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <returns>A read-only list of all of the user's notifications.</returns>
         [HttpGet("{UserID}")]
-        [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)]
-        public NotificationResultDto GetNotifications(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<NotificationResultDto> GetNotifications(
             [FromRoute] string userId,
             [FromQuery] bool? isRead,
             [FromQuery] int? startIndex,
@@ -59,8 +59,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user's ID.</param>
         /// <returns>Notifications summary for the user.</returns>
         [HttpGet("{UserID}/Summary")]
-        [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)]
-        public NotificationsSummaryDto GetNotificationsSummary(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
             [FromRoute] string userId)
         {
             return new NotificationsSummaryDto();
@@ -71,8 +71,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>All notification types.</returns>
         [HttpGet("Types")]
-        [ProducesResponseType(typeof(IEnumerable<NotificationTypeInfo>), StatusCodes.Status200OK)]
-        public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<NotificationTypeInfo>> GetNotificationTypes()
         {
             return _notificationManager.GetNotificationTypes();
         }
@@ -82,10 +82,10 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>All notification services.</returns>
         [HttpGet("Services")]
-        [ProducesResponseType(typeof(IEnumerable<NameIdPair>), StatusCodes.Status200OK)]
-        public IEnumerable<NameIdPair> GetNotificationServices()
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<NameIdPair>> GetNotificationServices()
         {
-            return _notificationManager.GetNotificationServices();
+            return _notificationManager.GetNotificationServices().ToList();
         }
 
         /// <summary>
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="level">The level of the notification.</param>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public void CreateAdminNotification(
+        public ActionResult CreateAdminNotification(
             [FromQuery] string name,
             [FromQuery] string description,
             [FromQuery] string? url,
@@ -114,6 +114,8 @@ namespace Jellyfin.Api.Controllers
             };
 
             _notificationManager.SendNotification(notification, CancellationToken.None);
+
+            return Ok();
         }
 
         /// <summary>
@@ -123,10 +125,11 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
         [HttpPost("{UserID}/Read")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public void SetRead(
+        public ActionResult SetRead(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
+            return Ok();
         }
 
         /// <summary>
@@ -136,10 +139,11 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
         [HttpPost("{UserID}/Unread")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public void SetUnread(
+        public ActionResult SetUnread(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
+            return Ok();
         }
     }
 }

From a06d271725f6e746d9a970f29283ab8f3ebae607 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 22 Apr 2020 13:07:21 -0600
Subject: [PATCH 060/463] Move ConfigurationService to Jellyfin.Api

---
 .../Controllers/ConfigurationController.cs    | 128 +++++++++++++++
 .../ConfigurationDtos/MediaEncoderPathDto.cs  |  18 +++
 MediaBrowser.Api/ConfigurationService.cs      | 146 ------------------
 3 files changed, 146 insertions(+), 146 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ConfigurationController.cs
 create mode 100644 Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
 delete mode 100644 MediaBrowser.Api/ConfigurationService.cs

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
new file mode 100644
index 0000000000..14e45833f0
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -0,0 +1,128 @@
+#nullable enable
+
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.ConfigurationDtos;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Configuration Controller.
+    /// </summary>
+    [Route("System")]
+    [Authenticated]
+    public class ConfigurationController : BaseJellyfinApiController
+    {
+        private readonly IServerConfigurationManager _configurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
+        /// </summary>
+        /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="jsonSerializer">Instance of the <see cref="IJsonSerializer"/> interface.</param>
+        public ConfigurationController(
+            IServerConfigurationManager configurationManager,
+            IMediaEncoder mediaEncoder,
+            IJsonSerializer jsonSerializer)
+        {
+            _configurationManager = configurationManager;
+            _mediaEncoder = mediaEncoder;
+            _jsonSerializer = jsonSerializer;
+        }
+
+        /// <summary>
+        /// Gets application configuration.
+        /// </summary>
+        /// <returns>Application configuration.</returns>
+        [HttpGet("Configuration")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ServerConfiguration> GetConfiguration()
+        {
+            return Ok(_configurationManager.Configuration);
+        }
+
+        /// <summary>
+        /// Updates application configuration.
+        /// </summary>
+        /// <param name="configuration">Configuration.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Configuration")]
+        [Authenticated(Roles = "Admin")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
+        {
+            _configurationManager.ReplaceConfiguration(configuration);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets a named configuration.
+        /// </summary>
+        /// <param name="key">Configuration key.</param>
+        /// <returns>Configuration.</returns>
+        [HttpGet("Configuration/{Key}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
+        {
+            return Ok(_configurationManager.GetConfiguration(key));
+        }
+
+        /// <summary>
+        /// Updates named configuration.
+        /// </summary>
+        /// <param name="key">Configuration key.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("Configuration/{Key}")]
+        [Authenticated(Roles = "Admin")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
+        {
+            var configurationType = _configurationManager.GetConfigurationType(key);
+            /*
+            // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed.
+            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType);
+            */
+
+            var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType)
+                .ConfigureAwait(false);
+            _configurationManager.SaveConfiguration(key, configuration);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets a default MetadataOptions object.
+        /// </summary>
+        /// <returns>MetadataOptions.</returns>
+        [HttpGet("Configuration/MetadataOptions/Default")]
+        [Authenticated(Roles = "Admin")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
+        {
+            return Ok(new MetadataOptions());
+        }
+
+        /// <summary>
+        /// Updates the path to the media encoder.
+        /// </summary>
+        /// <param name="mediaEncoderPath">Media encoder path form body.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("MediaEncoder/Path")]
+        [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
+        {
+            _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
+            return Ok();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
new file mode 100644
index 0000000000..b05e0cdf5a
--- /dev/null
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.ConfigurationDtos
+{
+    /// <summary>
+    /// Media Encoder Path Dto.
+    /// </summary>
+    public class MediaEncoderPathDto
+    {
+        /// <summary>
+        /// Gets or sets media encoder path.
+        /// </summary>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets media encoder path type.
+        /// </summary>
+        public string PathType { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs
deleted file mode 100644
index 316be04a03..0000000000
--- a/MediaBrowser.Api/ConfigurationService.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetConfiguration
-    /// </summary>
-    [Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
-    [Authenticated]
-    public class GetConfiguration : IReturn<ServerConfiguration>
-    {
-
-    }
-
-    [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetNamedConfiguration
-    {
-        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Key { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateConfiguration
-    /// </summary>
-    [Route("/System/Configuration", "POST", Summary = "Updates application configuration")]
-    [Authenticated(Roles = "Admin")]
-    public class UpdateConfiguration : ServerConfiguration, IReturnVoid
-    {
-    }
-
-    [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")]
-    [Authenticated(Roles = "Admin")]
-    public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream
-    {
-        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Key { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDefaultMetadataOptions : IReturn<MetadataOptions>
-    {
-
-    }
-
-    [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")]
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class UpdateMediaEncoderPath : IReturnVoid
-    {
-        [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Path { get; set; }
-        [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string PathType { get; set; }
-    }
-
-    public class ConfigurationService : BaseApiService
-    {
-        /// <summary>
-        /// The _json serializer
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// The _configuration manager
-        /// </summary>
-        private readonly IServerConfigurationManager _configurationManager;
-
-        private readonly IMediaEncoder _mediaEncoder;
-
-        public ConfigurationService(
-            ILogger<ConfigurationService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IServerConfigurationManager configurationManager,
-            IMediaEncoder mediaEncoder)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _jsonSerializer = jsonSerializer;
-            _configurationManager = configurationManager;
-            _mediaEncoder = mediaEncoder;
-        }
-
-        public void Post(UpdateMediaEncoderPath request)
-        {
-            _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetConfiguration request)
-        {
-            return ToOptimizedResult(_configurationManager.Configuration);
-        }
-
-        public object Get(GetNamedConfiguration request)
-        {
-            var result = _configurationManager.GetConfiguration(request.Key);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified configuraiton.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateConfiguration request)
-        {
-            // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
-            var json = _jsonSerializer.SerializeToString(request);
-
-            var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
-
-            _configurationManager.ReplaceConfiguration(config);
-        }
-
-        public async Task Post(UpdateNamedConfiguration request)
-        {
-            var key = GetPathValue(2).ToString();
-
-            var configurationType = _configurationManager.GetConfigurationType(key);
-            var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false);
-
-            _configurationManager.SaveConfiguration(key, configuration);
-        }
-
-        public object Get(GetDefaultMetadataOptions request)
-        {
-            return ToOptimizedResult(new MetadataOptions());
-        }
-    }
-}

From c6eebca335d09d6a6c627205e126448ab5441f37 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 07:29:28 -0600
Subject: [PATCH 061/463] Apply suggestions and add URL to log message

---
 .../Middleware/ExceptionMiddleware.cs         | 34 +++++++++++--------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 0d9dac89f0..ecc76594e4 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -1,5 +1,6 @@
 using System;
 using System.IO;
+using System.Net.Mime;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
@@ -22,15 +23,15 @@ namespace Jellyfin.Server.Middleware
         /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
         /// </summary>
         /// <param name="next">Next request delegate.</param>
-        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         public ExceptionMiddleware(
             RequestDelegate next,
-            ILoggerFactory loggerFactory,
+            ILogger<ExceptionMiddleware> logger,
             IServerConfigurationManager serverConfigurationManager)
         {
             _next = next;
-            _logger = loggerFactory.CreateLogger<ExceptionMiddleware>();
+            _logger = logger;
             _configuration = serverConfigurationManager;
         }
 
@@ -54,9 +55,14 @@ namespace Jellyfin.Server.Middleware
                 }
 
                 ex = GetActualException(ex);
-                _logger.LogError(ex, "Error processing request: {0}", ex.Message);
+                _logger.LogError(
+                    ex,
+                    "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ",
+                    ex.Message,
+                    context.Request.Method,
+                    context.Request.Path);
                 context.Response.StatusCode = GetStatusCode(ex);
-                context.Response.ContentType = "text/plain";
+                context.Response.ContentType = MediaTypeNames.Text.Plain;
 
                 var errorContent = NormalizeExceptionMessage(ex.Message);
                 await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
@@ -105,16 +111,14 @@ namespace Jellyfin.Server.Middleware
             }
 
             // Strip any information we don't want to reveal
-            msg = msg.Replace(
-                _configuration.ApplicationPaths.ProgramSystemPath,
-                string.Empty,
-                StringComparison.OrdinalIgnoreCase);
-            msg = msg.Replace(
-                _configuration.ApplicationPaths.ProgramDataPath,
-                string.Empty,
-                StringComparison.OrdinalIgnoreCase);
-
-            return msg;
+            return msg.Replace(
+                    _configuration.ApplicationPaths.ProgramSystemPath,
+                    string.Empty,
+                    StringComparison.OrdinalIgnoreCase)
+                .Replace(
+                    _configuration.ApplicationPaths.ProgramDataPath,
+                    string.Empty,
+                    StringComparison.OrdinalIgnoreCase);
         }
     }
 }

From c7c2f9da90a7cf7a452de0ab1adf7e36f422bbe1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 07:51:04 -0600
Subject: [PATCH 062/463] Apply suggestions

---
 Jellyfin.Api/Controllers/StartupController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 2db7e32aad..14c59593fb 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
                 PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
             };
 
-            return Ok(result);
+            return result;
         }
 
         /// <summary>
@@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<StartupUserDto> GetFirstUser()
         {
             var user = _userManager.Users.First();
-            return Ok(new StartupUserDto { Name = user.Name, Password = user.Password });
+            return new StartupUserDto { Name = user.Name, Password = user.Password };
         }
 
         /// <summary>

From 4d894c4344fd23026bbfdc0a1cdd24231441a444 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 07:55:47 -0600
Subject: [PATCH 063/463] Remove unneeded Ok calls.

---
 Jellyfin.Api/Controllers/DevicesController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 559a260071..cebb51ccfe 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            return Ok(deviceInfo);
+            return deviceInfo;
         }
 
         /// <summary>
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceInfo> GetDeviceOptions([FromQuery, BindRequired] string id)
+        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string id)
         {
             var deviceInfo = _deviceManager.GetDeviceOptions(id);
             if (deviceInfo == null)
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            return Ok(deviceInfo);
+            return deviceInfo;
         }
 
         /// <summary>
@@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<ContentUploadHistory> GetCameraUploads([FromQuery, BindRequired] string id)
         {
             var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
-            return Ok(uploadHistory);
+            return uploadHistory;
         }
 
         /// <summary>

From 1223eb5a2285c48f50b07fb5aa2c463928b69afe Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 08:03:41 -0600
Subject: [PATCH 064/463] Remove unneeded Ok calls.

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 25391bcf84..42e87edd6f 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            return Ok(result);
+            return result;
         }
 
         /// <summary>

From 5ca7e1fd79d85e7e531c747f4eca203ff862be8d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 08:54:28 -0600
Subject: [PATCH 065/463] Move ChannelService to Jellyfin.Api

---
 .../Controllers/ChannelsController.cs         | 238 ++++++++++++
 Jellyfin.Api/Extensions/RequestExtensions.cs  |  90 +++++
 MediaBrowser.Api/ChannelService.cs            | 341 ------------------
 3 files changed, 328 insertions(+), 341 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ChannelsController.cs
 create mode 100644 Jellyfin.Api/Extensions/RequestExtensions.cs
 delete mode 100644 MediaBrowser.Api/ChannelService.cs

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
new file mode 100644
index 0000000000..4e2621b7b3
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -0,0 +1,238 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Channels Controller.
+    /// </summary>
+    public class ChannelsController : BaseJellyfinApiController
+    {
+        private readonly IChannelManager _channelManager;
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ChannelsController"/> class.
+        /// </summary>
+        /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        public ChannelsController(IChannelManager channelManager, IUserManager userManager)
+        {
+            _channelManager = channelManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Gets available channels.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
+        /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
+        /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
+        /// <returns>Channels.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+            [FromQuery] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] bool? supportsLatestItems,
+            [FromQuery] bool? supportsMediaDeletion,
+            [FromQuery] bool? isFavorite)
+        {
+            return _channelManager.GetChannels(new ChannelQuery
+            {
+                Limit = limit,
+                StartIndex = startIndex,
+                UserId = userId,
+                SupportsLatestItems = supportsLatestItems,
+                SupportsMediaDeletion = supportsMediaDeletion,
+                IsFavorite = isFavorite
+            });
+        }
+
+        /// <summary>
+        /// Get all channel features.
+        /// </summary>
+        /// <returns>Channel features.</returns>
+        [HttpGet("Features")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<ChannelFeatures> GetAllChannelFeatures()
+        {
+            return _channelManager.GetAllChannelFeatures();
+        }
+
+        /// <summary>
+        /// Get channel features.
+        /// </summary>
+        /// <param name="id">Channel id.</param>
+        /// <returns>Channel features.</returns>
+        [HttpGet("{Id}/Features")]
+        public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string id)
+        {
+            return _channelManager.GetChannelFeatures(id);
+        }
+
+        /// <summary>
+        /// Get channel items.
+        /// </summary>
+        /// <param name="id">Channel Id.</param>
+        /// <param name="folderId">Folder Id.</param>
+        /// <param name="userId">User Id.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <returns>Channel items.</returns>
+        [HttpGet("{Id}/Items")]
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
+            [FromRoute] Guid id,
+            [FromQuery] Guid? folderId,
+            [FromQuery] Guid? userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string sortOrder,
+            [FromQuery] string filters,
+            [FromQuery] string sortBy,
+            [FromQuery] string fields)
+        {
+            var user = userId == null
+                ? null
+                : _userManager.GetUserById(userId.Value);
+
+            var query = new InternalItemsQuery(user)
+            {
+                Limit = limit,
+                StartIndex = startIndex,
+                ChannelIds = new[] { id },
+                ParentId = folderId ?? Guid.Empty,
+                OrderBy = RequestExtensions.GetOrderBy(sortBy, sortOrder),
+                DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) }
+            };
+
+            foreach (var filter in RequestExtensions.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                }
+            }
+
+            return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets latest channel items.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
+        /// <returns>Latest channel items.</returns>
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
+            [FromQuery] Guid? userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string filters,
+            [FromQuery] string fields,
+            [FromQuery] string channelIds)
+        {
+            var user = userId == null
+                ? null
+                : _userManager.GetUserById(userId.Value);
+
+            var query = new InternalItemsQuery(user)
+            {
+                Limit = limit,
+                StartIndex = startIndex,
+                ChannelIds =
+                    (channelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Select(i => new Guid(i)).ToArray(),
+                DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) }
+            };
+
+            foreach (var filter in RequestExtensions.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                }
+            }
+
+            return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Extensions/RequestExtensions.cs b/Jellyfin.Api/Extensions/RequestExtensions.cs
new file mode 100644
index 0000000000..b9d11dfefa
--- /dev/null
+++ b/Jellyfin.Api/Extensions/RequestExtensions.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Extensions
+{
+    /// <summary>
+    /// Request Extensions.
+    /// </summary>
+    public static class RequestExtensions
+    {
+        /// <summary>
+        /// Get Order By.
+        /// </summary>
+        /// <param name="sortBy">Sort By. Comma delimited string.</param>
+        /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
+        /// <returns>Order By.</returns>
+        public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
+        {
+            var val = sortBy;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return Array.Empty<ValueTuple<string, SortOrder>>();
+            }
+
+            var vals = val.Split(',');
+            if (string.IsNullOrWhiteSpace(requestedSortOrder))
+            {
+                requestedSortOrder = "Ascending";
+            }
+
+            var sortOrders = requestedSortOrder.Split(',');
+
+            var result = new ValueTuple<string, SortOrder>[vals.Length];
+
+            for (var i = 0; i < vals.Length; i++)
+            {
+                var sortOrderIndex = sortOrders.Length > i ? i : 0;
+
+                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
+                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
+                    ? SortOrder.Descending
+                    : SortOrder.Ascending;
+
+                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the item fields.
+        /// </summary>
+        /// <param name="fields">The fields.</param>
+        /// <returns>IEnumerable{ItemFields}.</returns>
+        public static ItemFields[] GetItemFields(string fields)
+        {
+            if (string.IsNullOrEmpty(fields))
+            {
+                return Array.Empty<ItemFields>();
+            }
+
+            return fields.Split(',').Select(v =>
+            {
+                if (Enum.TryParse(v, true, out ItemFields value))
+                {
+                    return (ItemFields?)value;
+                }
+
+                return null;
+            }).Where(i => i.HasValue).Select(i => i.Value).ToArray();
+        }
+
+        /// <summary>
+        /// Get parsed filters.
+        /// </summary>
+        /// <param name="filters">The filters.</param>
+        /// <returns>Item filters.</returns>
+        public static IEnumerable<ItemFilter> GetFilters(string filters)
+        {
+            return string.IsNullOrEmpty(filters)
+                ? Array.Empty<ItemFilter>()
+                : filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
+        }
+    }
+}
diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs
deleted file mode 100644
index fd9b8c3968..0000000000
--- a/MediaBrowser.Api/ChannelService.cs
+++ /dev/null
@@ -1,341 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Channels;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Channels", "GET", Summary = "Gets available channels")]
-    public class GetChannels : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SupportsLatestItems", Description = "Optional. Filter by channels that support getting latest items.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? SupportsLatestItems { get; set; }
-
-        public bool? SupportsMediaDeletion { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is favorite.
-        /// </summary>
-        /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
-        public bool? IsFavorite { get; set; }
-    }
-
-    [Route("/Channels/{Id}/Features", "GET", Summary = "Gets features for a channel")]
-    public class GetChannelFeatures : IReturn<ChannelFeatures>
-    {
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Channels/Features", "GET", Summary = "Gets features for a channel")]
-    public class GetAllChannelFeatures : IReturn<ChannelFeatures[]>
-    {
-    }
-
-    [Route("/Channels/{Id}/Items", "GET", Summary = "Gets channel items")]
-    public class GetChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
-    {
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "FolderId", Description = "Folder Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string FolderId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public IEnumerable<ItemFilter> GetFilters()
-        {
-            var val = Filters;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ItemFilter>()
-                : val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
-        }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public ValueTuple<string, SortOrder>[] GetOrderBy()
-        {
-            return BaseItemsRequest.GetOrderBy(SortBy, SortOrder);
-        }
-    }
-
-    [Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")]
-    public class GetLatestChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "ChannelIds", Description = "Optional. Specify one or more channel id's, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ChannelIds { get; set; }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public IEnumerable<ItemFilter> GetFilters()
-        {
-            return string.IsNullOrEmpty(Filters)
-                ? Array.Empty<ItemFilter>()
-                : Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
-        }
-    }
-
-    [Authenticated]
-    public class ChannelService : BaseApiService
-    {
-        private readonly IChannelManager _channelManager;
-        private IUserManager _userManager;
-
-        public ChannelService(
-            ILogger<ChannelService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IChannelManager channelManager,
-            IUserManager userManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _channelManager = channelManager;
-            _userManager = userManager;
-        }
-
-        public object Get(GetAllChannelFeatures request)
-        {
-            var result = _channelManager.GetAllChannelFeatures();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetChannelFeatures request)
-        {
-            var result = _channelManager.GetChannelFeatures(request.Id);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetChannels request)
-        {
-            var result = _channelManager.GetChannels(new ChannelQuery
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                SupportsLatestItems = request.SupportsLatestItems,
-                SupportsMediaDeletion = request.SupportsMediaDeletion,
-                IsFavorite = request.IsFavorite
-            });
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetChannelItems request)
-        {
-            var user = request.UserId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                ChannelIds = new[] { new Guid(request.Id) },
-                ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId),
-                OrderBy = request.GetOrderBy(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = request.GetItemFields()
-                }
-
-            };
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetLatestChannelItems request)
-        {
-            var user = request.UserId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                ChannelIds = (request.ChannelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToArray(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = request.GetItemFields()
-                }
-            };
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-    }
-}

From bb8e738a0817be2e13a8b21929d0f0aeb0c6a461 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 10:03:54 -0600
Subject: [PATCH 066/463] Fix Authorize attributes

---
 .../Controllers/ConfigurationController.cs    | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 14e45833f0..b508ac0547 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,12 +1,13 @@
 #nullable enable
 
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.ConfigurationDtos;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Serialization;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// Configuration Controller.
     /// </summary>
     [Route("System")]
-    [Authenticated]
+    [Authorize]
     public class ConfigurationController : BaseJellyfinApiController
     {
         private readonly IServerConfigurationManager _configurationManager;
@@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ServerConfiguration> GetConfiguration()
         {
-            return Ok(_configurationManager.Configuration);
+            return _configurationManager.Configuration;
         }
 
         /// <summary>
@@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Configuration.</param>
         /// <returns>Status.</returns>
         [HttpPost("Configuration")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
         {
@@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
         {
-            return Ok(_configurationManager.GetConfiguration(key));
+            return _configurationManager.GetConfiguration(key);
         }
 
         /// <summary>
@@ -83,7 +84,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="key">Configuration key.</param>
         /// <returns>Status.</returns>
         [HttpPost("Configuration/{Key}")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
         {
@@ -104,11 +105,11 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>MetadataOptions.</returns>
         [HttpGet("Configuration/MetadataOptions/Default")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
         {
-            return Ok(new MetadataOptions());
+            return new MetadataOptions();
         }
 
         /// <summary>
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaEncoderPath">Media encoder path form body.</param>
         /// <returns>Status.</returns>
         [HttpPost("MediaEncoder/Path")]
-        [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
+        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
         {

From f3da5dc8b7fef7e5fdeddff941c6d99063a1fd97 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 10:04:37 -0600
Subject: [PATCH 067/463] Fix Authorize attributes

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index 351401de18..b0cdfb86e9 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authenticated]
+    [Authorize]
     public class AttachmentsController : Controller
     {
         private readonly ILibraryManager _libraryManager;

From 311f2e2bc317cea7ac4d4cc783b961793bb997d5 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 10:07:21 -0600
Subject: [PATCH 068/463] Fix Authorize attributes

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 42e87edd6f..0d375e668a 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -2,9 +2,9 @@
 
 using System.ComponentModel.DataAnnotations;
 using System.Threading;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -14,7 +14,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Display Preferences Controller.
     /// </summary>
-    [Authenticated]
+    [Authorize]
     public class DisplayPreferencesController : BaseJellyfinApiController
     {
         private readonly IDisplayPreferencesRepository _displayPreferencesRepository;

From 3c34d956088430da08bdd812c05d6a87c3bf9d25 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 21:23:29 -0600
Subject: [PATCH 069/463] Address comments

---
 .../Middleware/ExceptionMiddleware.cs         | 36 +++++++++++++++----
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index ecc76594e4..6ebe015030 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -1,8 +1,10 @@
 using System;
 using System.IO;
 using System.Net.Mime;
+using System.Net.Sockets;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
@@ -55,15 +57,35 @@ namespace Jellyfin.Server.Middleware
                 }
 
                 ex = GetActualException(ex);
-                _logger.LogError(
-                    ex,
-                    "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ",
-                    ex.Message,
-                    context.Request.Method,
-                    context.Request.Path);
+
+                bool ignoreStackTrace =
+                    ex is SocketException
+                    || ex is IOException
+                    || ex is OperationCanceledException
+                    || ex is SecurityException
+                    || ex is AuthenticationException
+                    || ex is FileNotFoundException;
+
+                if (ignoreStackTrace)
+                {
+                    _logger.LogError(
+                        "Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
+                        ex.Message.TrimEnd('.'),
+                        context.Request.Method,
+                        context.Request.Path);
+                }
+                else
+                {
+                    _logger.LogError(
+                        ex,
+                        "Error processing request. URL {Method} {Url}.",
+                        ex.Message.TrimEnd('.'),
+                        context.Request.Method,
+                        context.Request.Path);
+                }
+
                 context.Response.StatusCode = GetStatusCode(ex);
                 context.Response.ContentType = MediaTypeNames.Text.Plain;
-
                 var errorContent = NormalizeExceptionMessage(ex.Message);
                 await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
             }

From be50fae38a27878cb520e20ed7956a72d7b05a84 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 21:24:40 -0600
Subject: [PATCH 070/463] Address comments

---
 .../Extensions/ApiApplicationBuilderExtensions.cs      | 10 ----------
 Jellyfin.Server/Startup.cs                             |  3 ++-
 2 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 6c105ab65b..0bd654c7dc 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -24,15 +24,5 @@ namespace Jellyfin.Server.Extensions
                 c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1");
             });
         }
-
-        /// <summary>
-        /// Adds exception middleware to the application pipeline.
-        /// </summary>
-        /// <param name="applicationBuilder">The application builder.</param>
-        /// <returns>The updated application builder.</returns>
-        public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder)
-        {
-            return applicationBuilder.UseMiddleware<ExceptionMiddleware>();
-        }
     }
 }
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 7a632f6c44..b17357fc3d 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,4 +1,5 @@
 using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Middleware;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
@@ -58,7 +59,7 @@ namespace Jellyfin.Server
                 app.UseDeveloperExceptionPage();
             }
 
-            app.UseExceptionMiddleware();
+            app.UseMiddleware<ExceptionMiddleware>();
 
             app.UseWebSockets();
 

From b8508a57d8320085c01a7e2d4656b233169584f2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 21:38:40 -0600
Subject: [PATCH 071/463] oop

---
 Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 6ebe015030..0d79bbfaff 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -79,7 +79,6 @@ namespace Jellyfin.Server.Middleware
                     _logger.LogError(
                         ex,
                         "Error processing request. URL {Method} {Url}.",
-                        ex.Message.TrimEnd('.'),
                         context.Request.Method,
                         context.Request.Path);
                 }

From 0765ef8bda4d23e33fde7a1bfe49b5a365c6d28e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 21:41:10 -0600
Subject: [PATCH 072/463] Apply suggestions, fix warning

---
 Jellyfin.Server/Models/JsonOptions.cs | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs
index fa503bc9a4..2f0df3d2c7 100644
--- a/Jellyfin.Server/Models/JsonOptions.cs
+++ b/Jellyfin.Server/Models/JsonOptions.cs
@@ -7,11 +7,6 @@ namespace Jellyfin.Server.Models
     /// </summary>
     public static class JsonOptions
     {
-        /// <summary>
-        /// Base Json Serializer Options.
-        /// </summary>
-        private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions();
-
         /// <summary>
         /// Gets CamelCase json options.
         /// </summary>
@@ -19,7 +14,7 @@ namespace Jellyfin.Server.Models
         {
             get
             {
-                var options = _jsonOptions;
+                var options = DefaultJsonOptions;
                 options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
                 return options;
             }
@@ -32,10 +27,15 @@ namespace Jellyfin.Server.Models
         {
             get
             {
-                var options = _jsonOptions;
+                var options = DefaultJsonOptions;
                 options.PropertyNamingPolicy = null;
                 return options;
             }
         }
+
+        /// <summary>
+        /// Gets base Json Serializer Options.
+        /// </summary>
+        private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions();
     }
 }

From 85853f9ce3d77469b84e3334d7080cd025474ee8 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Fri, 24 Apr 2020 17:11:11 -0600
Subject: [PATCH 073/463] Add back in return type documentation

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 0bf3aa1b47..8da2a6c536 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -95,6 +95,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
+        /// <returns>Status.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult CreateAdminNotification(
@@ -123,6 +124,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
+        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Read")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult SetRead(
@@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
+        /// <returns>Status.</returns>
         [HttpPost("{UserID}/Unread")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult SetUnread(

From 714aaefbcc3abf0b952efc831001cf42e1c873b0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 24 Apr 2020 18:20:36 -0600
Subject: [PATCH 074/463] Transfer EnvironmentService to Jellyfin.Api

---
 .../Controllers/EnvironmentController.cs      | 200 ++++++++++++
 .../DefaultDirectoryBrowserInfo.cs            |  13 +
 .../Models/EnvironmentDtos/ValidatePathDto.cs |  23 ++
 MediaBrowser.Api/EnvironmentService.cs        | 296 ------------------
 4 files changed, 236 insertions(+), 296 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/EnvironmentController.cs
 create mode 100644 Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs
 create mode 100644 Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
 delete mode 100644 MediaBrowser.Api/EnvironmentService.cs

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
new file mode 100644
index 0000000000..139c1af083
--- /dev/null
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -0,0 +1,200 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.EnvironmentDtos;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Environment Controller.
+    /// </summary>
+    [Authorize(Policy = Policies.RequiresElevation)]
+    public class EnvironmentController : BaseJellyfinApiController
+    {
+        private const char UncSeparator = '\\';
+        private const string UncSeparatorString = "\\";
+
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
+        /// </summary>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        public EnvironmentController(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Gets the contents of a given directory in the file system.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
+        /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
+        /// <returns>File system entries.</returns>
+        [HttpGet("DirectoryContents")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
+            [FromQuery, BindRequired] string path,
+            [FromQuery] bool includeFiles,
+            [FromQuery] bool includeDirectories)
+        {
+            const string networkPrefix = UncSeparatorString + UncSeparatorString;
+            if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase)
+                && path.LastIndexOf(UncSeparator) == 1)
+            {
+                return Array.Empty<FileSystemEntryInfo>();
+            }
+
+            var entries = _fileSystem.GetFileSystemEntries(path).OrderBy(i => i.FullName).Where(i =>
+            {
+                var isDirectory = i.IsDirectory;
+
+                if (!includeFiles && !isDirectory)
+                {
+                    return false;
+                }
+
+                return includeDirectories || !isDirectory;
+            });
+
+            return entries.Select(f => new FileSystemEntryInfo
+            {
+                Name = f.Name,
+                Path = f.FullName,
+                Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File
+            });
+        }
+
+        /// <summary>
+        /// Validates path.
+        /// </summary>
+        /// <param name="validatePathDto">Validate request object.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("ValidatePath")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto)
+        {
+            if (validatePathDto.IsFile.HasValue)
+            {
+                if (validatePathDto.IsFile.Value)
+                {
+                    if (!System.IO.File.Exists(validatePathDto.Path))
+                    {
+                        return NotFound();
+                    }
+                }
+                else
+                {
+                    if (!Directory.Exists(validatePathDto.Path))
+                    {
+                        return NotFound();
+                    }
+                }
+            }
+            else
+            {
+                if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+                {
+                    return NotFound();
+                }
+
+                if (validatePathDto.ValidateWritable)
+                {
+                    var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
+                    try
+                    {
+                        System.IO.File.WriteAllText(file, string.Empty);
+                    }
+                    finally
+                    {
+                        if (System.IO.File.Exists(file))
+                        {
+                            System.IO.File.Delete(file);
+                        }
+                    }
+                }
+            }
+
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets network paths.
+        /// </summary>
+        /// <returns>List of entries.</returns>
+        [Obsolete("This endpoint is obsolete.")]
+        [HttpGet("NetworkShares")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
+        {
+            return Array.Empty<FileSystemEntryInfo>();
+        }
+
+        /// <summary>
+        /// Gets available drives from the server's file system.
+        /// </summary>
+        /// <returns>List of entries.</returns>
+        [HttpGet("Drives")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<FileSystemEntryInfo> GetDrives()
+        {
+            return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo
+            {
+                Name = d.Name,
+                Path = d.FullName,
+                Type = FileSystemEntryType.Directory
+            });
+        }
+
+        /// <summary>
+        /// Gets the parent path of a given path.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>Parent path.</returns>
+        [HttpGet("ParentPath")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<string?> GetParentPath([FromQuery, BindRequired] string path)
+        {
+            string? parent = Path.GetDirectoryName(path);
+            if (string.IsNullOrEmpty(parent))
+            {
+                // Check if unc share
+                var index = path.LastIndexOf(UncSeparator);
+
+                if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
+                {
+                    parent = path.Substring(0, index);
+
+                    if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
+                    {
+                        parent = null;
+                    }
+                }
+            }
+
+            return parent;
+        }
+
+        /// <summary>
+        /// Get Default directory browser.
+        /// </summary>
+        /// <returns>Default directory browser.</returns>
+        [HttpGet("DefaultDirectoryBrowser")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<DefaultDirectoryBrowserInfo> GetDefaultDirectoryBrowser()
+        {
+            return new DefaultDirectoryBrowserInfo();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs
new file mode 100644
index 0000000000..6b1c750bf6
--- /dev/null
+++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.EnvironmentDtos
+{
+    /// <summary>
+    /// Default directory browser info.
+    /// </summary>
+    public class DefaultDirectoryBrowserInfo
+    {
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        public string Path { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
new file mode 100644
index 0000000000..60c82e166b
--- /dev/null
+++ b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.EnvironmentDtos
+{
+    /// <summary>
+    /// Validate path object.
+    /// </summary>
+    public class ValidatePathDto
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether validate if path is writable.
+        /// </summary>
+        public bool ValidateWritable { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets is path file.
+        /// </summary>
+        public bool? IsFile { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs
deleted file mode 100644
index d199ce1544..0000000000
--- a/MediaBrowser.Api/EnvironmentService.cs
+++ /dev/null
@@ -1,296 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetDirectoryContents
-    /// </summary>
-    [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")]
-    public class GetDirectoryContents : IReturn<List<FileSystemEntryInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [include files].
-        /// </summary>
-        /// <value><c>true</c> if [include files]; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "IncludeFiles", Description = "An optional filter to include or exclude files from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool IncludeFiles { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [include directories].
-        /// </summary>
-        /// <value><c>true</c> if [include directories]; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "IncludeDirectories", Description = "An optional filter to include or exclude folders from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool IncludeDirectories { get; set; }
-    }
-
-    [Route("/Environment/ValidatePath", "POST", Summary = "Gets the contents of a given directory in the file system")]
-    public class ValidatePath
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Path { get; set; }
-
-        public bool ValidateWriteable { get; set; }
-        public bool? IsFile { get; set; }
-    }
-
-    [Obsolete]
-    [Route("/Environment/NetworkShares", "GET", Summary = "Gets shares from a network device")]
-    public class GetNetworkShares : IReturn<List<FileSystemEntryInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetDrives
-    /// </summary>
-    [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")]
-    public class GetDrives : IReturn<List<FileSystemEntryInfo>>
-    {
-    }
-
-    /// <summary>
-    /// Class GetNetworkComputers
-    /// </summary>
-    [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")]
-    public class GetNetworkDevices : IReturn<List<FileSystemEntryInfo>>
-    {
-    }
-
-    [Route("/Environment/ParentPath", "GET", Summary = "Gets the parent path of a given path")]
-    public class GetParentPath : IReturn<string>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-    }
-
-    public class DefaultDirectoryBrowserInfo
-    {
-        public string Path { get; set; }
-    }
-
-    [Route("/Environment/DefaultDirectoryBrowser", "GET", Summary = "Gets the parent path of a given path")]
-    public class GetDefaultDirectoryBrowser : IReturn<DefaultDirectoryBrowserInfo>
-    {
-
-    }
-
-    /// <summary>
-    /// Class EnvironmentService
-    /// </summary>
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class EnvironmentService : BaseApiService
-    {
-        private const char UncSeparator = '\\';
-        private const string UncSeparatorString = "\\";
-
-        /// <summary>
-        /// The _network manager
-        /// </summary>
-        private readonly INetworkManager _networkManager;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="EnvironmentService" /> class.
-        /// </summary>
-        /// <param name="networkManager">The network manager.</param>
-        public EnvironmentService(
-            ILogger<EnvironmentService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            INetworkManager networkManager,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _networkManager = networkManager;
-            _fileSystem = fileSystem;
-        }
-
-        public void Post(ValidatePath request)
-        {
-            if (request.IsFile.HasValue)
-            {
-                if (request.IsFile.Value)
-                {
-                    if (!File.Exists(request.Path))
-                    {
-                        throw new FileNotFoundException("File not found", request.Path);
-                    }
-                }
-                else
-                {
-                    if (!Directory.Exists(request.Path))
-                    {
-                        throw new FileNotFoundException("File not found", request.Path);
-                    }
-                }
-            }
-
-            else
-            {
-                if (!File.Exists(request.Path) && !Directory.Exists(request.Path))
-                {
-                    throw new FileNotFoundException("Path not found", request.Path);
-                }
-
-                if (request.ValidateWriteable)
-                {
-                    EnsureWriteAccess(request.Path);
-                }
-            }
-        }
-
-        protected void EnsureWriteAccess(string path)
-        {
-            var file = Path.Combine(path, Guid.NewGuid().ToString());
-
-            File.WriteAllText(file, string.Empty);
-            _fileSystem.DeleteFile(file);
-        }
-
-        public object Get(GetDefaultDirectoryBrowser request) =>
-            ToOptimizedResult(new DefaultDirectoryBrowserInfo { Path = null });
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetDirectoryContents request)
-        {
-            var path = request.Path;
-
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(Path));
-            }
-
-            var networkPrefix = UncSeparatorString + UncSeparatorString;
-
-            if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase)
-                && path.LastIndexOf(UncSeparator) == 1)
-            {
-                return ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-            }
-
-            return ToOptimizedResult(GetFileSystemEntries(request).ToList());
-        }
-
-        [Obsolete]
-        public object Get(GetNetworkShares request)
-            => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetDrives request)
-        {
-            var result = GetDrives().ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the list that is returned when an empty path is supplied
-        /// </summary>
-        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
-        private IEnumerable<FileSystemEntryInfo> GetDrives()
-        {
-            return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo
-            {
-                Name = d.Name,
-                Path = d.FullName,
-                Type = FileSystemEntryType.Directory
-            });
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetNetworkDevices request)
-            => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
-        /// <summary>
-        /// Gets the file system entries.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
-        private IEnumerable<FileSystemEntryInfo> GetFileSystemEntries(GetDirectoryContents request)
-        {
-            var entries = _fileSystem.GetFileSystemEntries(request.Path).OrderBy(i => i.FullName).Where(i =>
-            {
-                var isDirectory = i.IsDirectory;
-
-                if (!request.IncludeFiles && !isDirectory)
-                {
-                    return false;
-                }
-
-                return request.IncludeDirectories || !isDirectory;
-            });
-
-            return entries.Select(f => new FileSystemEntryInfo
-            {
-                Name = f.Name,
-                Path = f.FullName,
-                Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File
-
-            });
-        }
-
-        public object Get(GetParentPath request)
-        {
-            var parent = Path.GetDirectoryName(request.Path);
-
-            if (string.IsNullOrEmpty(parent))
-            {
-                // Check if unc share
-                var index = request.Path.LastIndexOf(UncSeparator);
-
-                if (index != -1 && request.Path.IndexOf(UncSeparator) == 0)
-                {
-                    parent = request.Path.Substring(0, index);
-
-                    if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
-                    {
-                        parent = null;
-                    }
-                }
-            }
-
-            return parent;
-        }
-    }
-}

From c7fe8b04cc854df110528a0eda4383263e99e554 Mon Sep 17 00:00:00 2001
From: Bruce <bruce.coelho93@gmail.com>
Date: Sat, 25 Apr 2020 19:59:31 +0100
Subject: [PATCH 075/463] PackageService to Jellyfin.API

---
 Jellyfin.Api/Controllers/PackageController.cs | 115 ++++++++++++
 MediaBrowser.Api/PackageService.cs            | 171 ------------------
 2 files changed, 115 insertions(+), 171 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/PackageController.cs
 delete mode 100644 MediaBrowser.Api/PackageService.cs

diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
new file mode 100644
index 0000000000..1fb9ab697d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -0,0 +1,115 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Updates;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Package Controller.
+    /// </summary>
+    [Route("Packages")]
+    [Authorize]
+    public class PackageController : BaseJellyfinApiController
+    {
+        private readonly IInstallationManager _installationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PackageController"/> class.
+        /// </summary>
+        /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>Installation Manager.</param>
+        public PackageController(IInstallationManager installationManager)
+        {
+            _installationManager = installationManager;
+        }
+
+        /// <summary>
+        /// Gets a package, by name or assembly guid.
+        /// </summary>
+        /// <param name="name">The name of the package.</param>
+        /// <param name="assemblyGuid">The guid of the associated assembly.</param>
+        /// <returns>Package info.</returns>
+        [HttpGet("/{Name}")]
+        [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
+        public ActionResult<PackageInfo> GetPackageInfo(
+            [FromRoute] [Required] string name,
+            [FromQuery] string? assemblyGuid)
+        {
+            var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult();
+            var result = _installationManager.FilterPackages(
+                packages,
+                name,
+                string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
+
+            return Ok(result);
+        }
+
+        /// <summary>
+        /// Gets available packages.
+        /// </summary>
+        /// <returns>Packages information.</returns>
+        [HttpGet]
+        [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
+        public async Task<ActionResult<PackageInfo[]>> GetPackages()
+        {
+            IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+
+            return Ok(packages.ToArray());
+        }
+
+        /// <summary>
+        /// Installs a package.
+        /// </summary>
+        /// <param name="name">Package name.</param>
+        /// <param name="assemblyGuid">Guid of the associated assembly.</param>
+        /// <param name="version">Optional version. Defaults to latest version.</param>
+        /// <returns>Status.</returns>
+        [HttpPost("/Installed/{Name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> InstallPackage(
+            [FromRoute] [Required] string name,
+            [FromQuery] string assemblyGuid,
+            [FromQuery] string version)
+        {
+            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+            var package = _installationManager.GetCompatibleVersions(
+                    packages,
+                    name,
+                    string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
+                    string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
+
+            if (package == null)
+            {
+                return NotFound();
+            }
+
+            await _installationManager.InstallPackage(package).ConfigureAwait(false);
+
+            return Ok();
+        }
+
+        /// <summary>
+        /// Cancels a package installation.
+        /// </summary>
+        /// <param name="id">Installation Id.</param>
+        /// <returns>Status.</returns>
+        [HttpDelete("/Installing/{id}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public IActionResult CancelPackageInstallation(
+            [FromRoute] [Required] string id)
+        {
+            _installationManager.CancelInstallation(new Guid(id));
+
+            return Ok();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs
deleted file mode 100644
index 444354a992..0000000000
--- a/MediaBrowser.Api/PackageService.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetPackage
-    /// </summary>
-    [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")]
-    [Authenticated]
-    public class GetPackage : IReturn<PackageInfo>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AssemblyGuid { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPackages
-    /// </summary>
-    [Route("/Packages", "GET", Summary = "Gets available packages")]
-    [Authenticated]
-    public class GetPackages : IReturn<PackageInfo[]>
-    {
-    }
-
-    /// <summary>
-    /// Class InstallPackage
-    /// </summary>
-    [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")]
-    [Authenticated(Roles = "Admin")]
-    public class InstallPackage : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string AssemblyGuid { get; set; }
-
-        /// <summary>
-        /// Gets or sets the version.
-        /// </summary>
-        /// <value>The version.</value>
-        [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Version { get; set; }
-    }
-
-    /// <summary>
-    /// Class CancelPackageInstallation
-    /// </summary>
-    [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")]
-    [Authenticated(Roles = "Admin")]
-    public class CancelPackageInstallation : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class PackageService
-    /// </summary>
-    public class PackageService : BaseApiService
-    {
-        private readonly IInstallationManager _installationManager;
-
-        public PackageService(
-            ILogger<PackageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IInstallationManager installationManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _installationManager = installationManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPackage request)
-        {
-            var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult();
-            var result = _installationManager.FilterPackages(
-                packages,
-                request.Name,
-                string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetPackages request)
-        {
-            IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
-
-            return ToOptimizedResult(packages.ToArray());
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException"></exception>
-        public async Task Post(InstallPackage request)
-        {
-            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
-            var package = _installationManager.GetCompatibleVersions(
-                    packages,
-                    request.Name,
-                    string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid),
-                    string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault();
-
-            if (package == null)
-            {
-                throw new ResourceNotFoundException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Package not found: {0}",
-                        request.Name));
-            }
-
-            await _installationManager.InstallPackage(package);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(CancelPackageInstallation request)
-        {
-            _installationManager.CancelInstallation(new Guid(request.Id));
-        }
-    }
-}

From f66714561e0fef18ba25c36abdf97ee62ccda007 Mon Sep 17 00:00:00 2001
From: Bruce Coelho <bruce.coelho93@gmail.com>
Date: Sat, 25 Apr 2020 21:32:49 +0100
Subject: [PATCH 076/463] Update Jellyfin.Api/Controllers/PackageController.cs

Applying requested changes to PackageController

Co-Authored-By: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/PackageController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 1fb9ab697d..ab4d204583 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
                 name,
                 string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
 
-            return Ok(result);
+            return result;
         }
 
         /// <summary>

From 5aced0ea0f4bca17aee392698351d54b0ad50e26 Mon Sep 17 00:00:00 2001
From: Bruce Coelho <bruce.coelho93@gmail.com>
Date: Sat, 25 Apr 2020 21:41:56 +0100
Subject: [PATCH 077/463] Apply suggestions from code review

Co-Authored-By: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/PackageController.cs | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index ab4d204583..1da5ac0e97 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -39,11 +39,11 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Package info.</returns>
         [HttpGet("/{Name}")]
         [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
-        public ActionResult<PackageInfo> GetPackageInfo(
+        public async Task<ActionResult<PackageInfo>> GetPackageInfo(
             [FromRoute] [Required] string name,
             [FromQuery] string? assemblyGuid)
         {
-            var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult();
+            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
             var result = _installationManager.FilterPackages(
                 packages,
                 name,
@@ -58,11 +58,11 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Packages information.</returns>
         [HttpGet]
         [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
-        public async Task<ActionResult<PackageInfo[]>> GetPackages()
+        public async Task<IEnumerable<PackageInfo>> GetPackages()
         {
             IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
 
-            return Ok(packages.ToArray());
+            return packages;
         }
 
         /// <summary>
@@ -75,6 +75,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Installed/{Name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> InstallPackage(
             [FromRoute] [Required] string name,
             [FromQuery] string assemblyGuid,

From 890e659cd390fc45c68b42c1a20f24a33e8c1570 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 25 Apr 2020 15:12:18 -0600
Subject: [PATCH 078/463] Fix autolaunch & redirect of swagger.

---
 Emby.Server.Implementations/Browser/BrowserLauncher.cs | 4 +++-
 Jellyfin.Server/Program.cs                             | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 96096e142a..384cb049fa 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -1,5 +1,7 @@
 using System;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Browser
@@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
         /// <param name="appHost">The app host.</param>
         public static void OpenSwaggerPage(IServerApplicationHost appHost)
         {
-            TryOpenUrl(appHost, "/swagger/index.html");
+            TryOpenUrl(appHost, "/api-docs/v1/swagger");
         }
 
         /// <summary>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index e55b0d4ed9..23ddcf159b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -529,7 +529,7 @@ namespace Jellyfin.Server
             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
             if (startupConfig != null && !startupConfig.HostWebClient())
             {
-                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html";
+                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger";
             }
 
             return config

From 000088f8f94e24ea715f15b722a2e64958bec07b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 25 Apr 2020 18:18:33 -0600
Subject: [PATCH 079/463] init

---
 Jellyfin.Api/Controllers/LibraryController.cs | 56 +++++++++++++++++++
 1 file changed, 56 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/LibraryController.cs

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
new file mode 100644
index 0000000000..f45101c0cb
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Library Controller.
+    /// </summary>
+    public class LibraryController : BaseJellyfinApiController
+    {
+        private readonly IProviderManager _providerManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IActivityManager _activityManager;
+        private readonly ILocalizationManager _localization;
+        private readonly ILibraryMonitor _libraryMonitor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LibraryController"/> class.
+        /// </summary>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
+        public LibraryController(
+            IProviderManager providerManager,
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService,
+            IAuthorizationContext authContext,
+            IActivityManager activityManager,
+            ILocalizationManager localization,
+            ILibraryMonitor libraryMonitor)
+        {
+            _providerManager = providerManager;
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+            _authContext = authContext;
+            _activityManager = activityManager;
+            _localization = localization;
+            _libraryMonitor = libraryMonitor;
+        }
+    }
+}

From 068368df6352cfad4e69df599c364b3f05b367ba Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 26 Apr 2020 23:28:32 -0600
Subject: [PATCH 080/463] Order actions by route, then http method

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 92bacb4400..00a73ade6f 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -105,6 +105,10 @@ namespace Jellyfin.Server.Extensions
                 {
                     c.IncludeXmlComments(xmlFile);
                 }
+
+                // Order actions by route path, then by http method.
+                c.OrderActionsBy(description =>
+                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}");
             });
         }
     }

From c61a200c9de2714b3d6353f3a4ae52b8962d369a Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Tue, 28 Apr 2020 09:30:59 -0600
Subject: [PATCH 081/463] Revise documentation based on discussion in #2872

---
 .../Controllers/NotificationsController.cs    | 35 +++++++++++--------
 1 file changed, 21 insertions(+), 14 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 8da2a6c536..8feea9ab61 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -35,13 +35,14 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for getting a user's notifications.
+        /// Gets a user's notifications.
         /// </summary>
         /// <param name="userId">The user's ID.</param>
         /// <param name="isRead">An optional filter by notification read state.</param>
         /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be omitted from the results.</param>
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
-        /// <returns>A read-only list of all of the user's notifications.</returns>
+        /// <response code="200">Notifications returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
         [HttpGet("{UserID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<NotificationResultDto> GetNotifications(
@@ -54,10 +55,11 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for getting a user's notification summary.
+        /// Gets a user's notification summary.
         /// </summary>
         /// <param name="userId">The user's ID.</param>
-        /// <returns>Notifications summary for the user.</returns>
+        /// <response code="200">Summary of user's notifications returned.</response>
+        /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
         [HttpGet("{UserID}/Summary")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
@@ -67,9 +69,10 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for getting notification types.
+        /// Gets notification types.
         /// </summary>
-        /// <returns>All notification types.</returns>
+        /// <response code="200">All notification types returned.</response>
+        /// <returns>An <cref see="OkResult"/> containing a list of all notification types.</returns>
         [HttpGet("Types")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NotificationTypeInfo>> GetNotificationTypes()
@@ -78,9 +81,10 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for getting notification services.
+        /// Gets notification services.
         /// </summary>
-        /// <returns>All notification services.</returns>
+        /// <response>All notification services returned.</response>
+        /// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
         [HttpGet("Services")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetNotificationServices()
@@ -89,13 +93,14 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint to send a notification to all admins.
+        /// Sends a notification to all admins.
         /// </summary>
         /// <param name="name">The name of the notification.</param>
         /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Notification sent.</response>
+        /// <returns>An <cref see="OkResult"/>.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult CreateAdminNotification(
@@ -120,11 +125,12 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint to set notifications as read.
+        /// Sets notifications as read.
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Notifications set as read.</response>
+        /// <returns>An <cref see="OkResult"/>.</returns>
         [HttpPost("{UserID}/Read")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult SetRead(
@@ -135,11 +141,12 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint to set notifications as unread.
+        /// Sets notifications as unread.
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Notifications set as unread.</response>
+        /// <returns>An <cref see="OkResult"/>.</returns>
         [HttpPost("{UserID}/Unread")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult SetUnread(

From 806ae1bc07e715c6109a3e8ec96c6d3dd6a802ef Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Apr 2020 08:04:05 -0600
Subject: [PATCH 082/463] Remove versioned API

---
 .../Browser/BrowserLauncher.cs                   |  2 +-
 .../ApiApplicationBuilderExtensions.cs           | 16 ++++++++--------
 .../Extensions/ApiServiceCollectionExtensions.cs |  2 +-
 Jellyfin.Server/Program.cs                       |  2 +-
 4 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 384cb049fa..e706401fd1 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
         /// <param name="appHost">The app host.</param>
         public static void OpenSwaggerPage(IServerApplicationHost appHost)
         {
-            TryOpenUrl(appHost, "/api-docs/v1/swagger");
+            TryOpenUrl(appHost, "/api-docs/swagger");
         }
 
         /// <summary>
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 33fd77d9c7..745567703f 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Configuration;
-using Jellyfin.Server.Middleware;
 using Microsoft.AspNetCore.Builder;
 
 namespace Jellyfin.Server.Extensions
@@ -31,19 +30,20 @@ namespace Jellyfin.Server.Extensions
             return applicationBuilder
                 .UseSwagger(c =>
                 {
-                    c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json";
+                    // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs'
+                    c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json";
                 })
                 .UseSwaggerUI(c =>
                 {
-                    c.DocumentTitle = "Jellyfin API v1";
-                    c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1");
-                    c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger";
+                    c.DocumentTitle = "Jellyfin API";
+                    c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
+                    c.RoutePrefix = $"{baseUrl}api-docs/swagger";
                 })
                 .UseReDoc(c =>
                 {
-                    c.DocumentTitle = "Jellyfin API v1";
-                    c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json");
-                    c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc";
+                    c.DocumentTitle = "Jellyfin API";
+                    c.SpecUrl($"/{baseUrl}api-docs/openapi.json");
+                    c.RoutePrefix = $"{baseUrl}api-docs/redoc";
                 });
         }
     }
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index a24785d57e..a354f45aad 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -96,7 +96,7 @@ namespace Jellyfin.Server.Extensions
         {
             return serviceCollection.AddSwaggerGen(c =>
             {
-                c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" });
 
                 // Add all xml doc files to swagger generator.
                 var xmlFiles = Directory.GetFiles(
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 23ddcf159b..7135800802 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -529,7 +529,7 @@ namespace Jellyfin.Server
             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
             if (startupConfig != null && !startupConfig.HostWebClient())
             {
-                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger";
+                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
             }
 
             return config

From 97ecffceb7fe655010c1f415fd688b3ee0f9d48d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Apr 2020 08:59:34 -0600
Subject: [PATCH 083/463] Add response code descriptions

---
 Jellyfin.Api/Controllers/StartupController.cs | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 14c59593fb..d60e46a01b 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Api endpoint for completing the startup wizard.
         /// </summary>
+        /// <response code="200">Startup wizard completed.</response>
         /// <returns>Status.</returns>
         [HttpPost("Complete")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -47,6 +48,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for getting the initial startup wizard configuration.
         /// </summary>
+        /// <response code="200">Initial startup wizard configuration retrieved.</response>
         /// <returns>The initial startup wizard configuration.</returns>
         [HttpGet("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -68,6 +70,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="uiCulture">The UI language culture.</param>
         /// <param name="metadataCountryCode">The metadata country code.</param>
         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
+        /// <response code="200">Configuration saved.</response>
         /// <returns>Status.</returns>
         [HttpPost("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -88,6 +91,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="enableRemoteAccess">Enable remote access.</param>
         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
+        /// <response code="200">Configuration saved.</response>
         /// <returns>Status.</returns>
         [HttpPost("RemoteAccess")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -102,6 +106,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Endpoint for returning the first user.
         /// </summary>
+        /// <response code="200">Initial user retrieved.</response>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -115,6 +120,7 @@ namespace Jellyfin.Api.Controllers
         /// Endpoint for updating the user name and password.
         /// </summary>
         /// <param name="startupUserDto">The DTO containing username and password.</param>
+        /// <response code="200">Updated user name and password.</response>
         /// <returns>The async task.</returns>
         [HttpPost("User")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 7a3925b863a12bea492a93f41cda4eb92dc9c183 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Apr 2020 09:41:12 -0600
Subject: [PATCH 084/463] Fix docs

---
 Jellyfin.Api/Controllers/StartupController.cs | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index d60e46a01b..66e4774aa0 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -31,7 +31,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Api endpoint for completing the startup wizard.
+        /// Completes the startup wizard.
         /// </summary>
         /// <response code="200">Startup wizard completed.</response>
         /// <returns>Status.</returns>
@@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for getting the initial startup wizard configuration.
+        /// Gets the initial startup wizard configuration.
         /// </summary>
         /// <response code="200">Initial startup wizard configuration retrieved.</response>
         /// <returns>The initial startup wizard configuration.</returns>
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for updating the initial startup wizard configuration.
+        /// Sets the initial startup wizard configuration.
         /// </summary>
         /// <param name="uiCulture">The UI language culture.</param>
         /// <param name="metadataCountryCode">The metadata country code.</param>
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for (dis)allowing remote access and UPnP.
+        /// Sets remote access and UPnP.
         /// </summary>
         /// <param name="enableRemoteAccess">Enable remote access.</param>
         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
@@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for returning the first user.
+        /// Gets the first user.
         /// </summary>
         /// <response code="200">Initial user retrieved.</response>
         /// <returns>The first user.</returns>
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Endpoint for updating the user name and password.
+        /// Sets the user name and password.
         /// </summary>
         /// <param name="startupUserDto">The DTO containing username and password.</param>
         /// <response code="200">Updated user name and password.</response>

From 82231b4393bb367f7fca50fed21f00e469b9f960 Mon Sep 17 00:00:00 2001
From: ZadenRB <zaden.ruggieroboune@gmail.com>
Date: Wed, 29 Apr 2020 15:53:29 -0600
Subject: [PATCH 085/463] Update to return IEnumerable directly where possible

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 8feea9ab61..3cbb3a3a3f 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <cref see="OkResult"/> containing a list of all notification types.</returns>
         [HttpGet("Types")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<NotificationTypeInfo>> GetNotificationTypes()
+        public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
         {
             return _notificationManager.GetNotificationTypes();
         }
@@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
         [HttpGet("Services")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<NameIdPair>> GetNotificationServices()
+        public IEnumerable<NameIdPair> GetNotificationServices()
         {
-            return _notificationManager.GetNotificationServices().ToList();
+            return _notificationManager.GetNotificationServices();
         }
 
         /// <summary>

From 0d8253d8e22d4cf34c58577e7fefb3f5733adedd Mon Sep 17 00:00:00 2001
From: Bruce <bruce.coelho93@gmail.com>
Date: Fri, 1 May 2020 15:17:40 +0100
Subject: [PATCH 086/463] Updated documentation according to discussion in
 jellyfin#2872

---
 Jellyfin.Api/Controllers/PackageController.cs | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 1da5ac0e97..b5ee47ee43 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -32,11 +32,11 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Gets a package, by name or assembly guid.
+        /// Gets a package by name or assembly guid.
         /// </summary>
         /// <param name="name">The name of the package.</param>
         /// <param name="assemblyGuid">The guid of the associated assembly.</param>
-        /// <returns>Package info.</returns>
+        /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
         [HttpGet("/{Name}")]
         [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
         public async Task<ActionResult<PackageInfo>> GetPackageInfo(
@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available packages.
         /// </summary>
-        /// <returns>Packages information.</returns>
+        /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
         [HttpGet]
         [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
         public async Task<IEnumerable<PackageInfo>> GetPackages()
@@ -71,7 +71,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="name">Package name.</param>
         /// <param name="assemblyGuid">Guid of the associated assembly.</param>
         /// <param name="version">Optional version. Defaults to latest version.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Package found.</response>
+        /// <response code="404">Package not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
         [HttpPost("/Installed/{Name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -102,7 +104,8 @@ namespace Jellyfin.Api.Controllers
         /// Cancels a package installation.
         /// </summary>
         /// <param name="id">Installation Id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Installation cancelled.</response>
+        /// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns>
         [HttpDelete("/Installing/{id}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         public IActionResult CancelPackageInstallation(

From 0017163f39438e2718f7c95b3fb65df5dde65e3d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 17:06:29 -0600
Subject: [PATCH 087/463] Update endpoint docs

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 0d375e668a..2837ea8e87 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -34,7 +34,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="displayPreferencesId">Display preferences id.</param>
         /// <param name="userId">User id.</param>
         /// <param name="client">Client.</param>
-        /// <returns>Display Preferences.</returns>
+        /// <response code="200">Display preferences retrieved.</response>
+        /// <response code="404">Specified display preferences not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
         [HttpGet("{DisplayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -59,7 +61,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User Id.</param>
         /// <param name="client">Client.</param>
         /// <param name="displayPreferences">New Display Preferences object.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Display preferences updated.</response>
+        /// <response code="404">Specified display preferences not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
         [HttpPost("{DisplayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)]

From f67daa84b04ae6c8ffcc42c038a65ecb8a433861 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 17:10:59 -0600
Subject: [PATCH 088/463] Update endpoint docs

---
 .../Controllers/ScheduledTasksController.cs   | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index da7cfbc3a7..ad70bf83b2 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -33,7 +33,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
         /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
-        /// <returns>Task list.</returns>
+        /// <response code="200">Scheduled tasks retrieved.</response>
+        /// <returns>The list of scheduled tasks.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<IScheduledTaskWorker> GetTasks(
@@ -65,7 +66,9 @@ namespace Jellyfin.Api.Controllers
         /// Get task by id.
         /// </summary>
         /// <param name="taskId">Task Id.</param>
-        /// <returns>Task Info.</returns>
+        /// <response code="200">Task retrieved.</response>
+        /// <response code="404">Task not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
         [HttpGet("{TaskID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -87,7 +90,9 @@ namespace Jellyfin.Api.Controllers
         /// Start specified task.
         /// </summary>
         /// <param name="taskId">Task Id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Task started.</response>
+        /// <response code="404">Task not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("Running/{TaskID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -109,7 +114,9 @@ namespace Jellyfin.Api.Controllers
         /// Stop specified task.
         /// </summary>
         /// <param name="taskId">Task Id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Task stopped.</response>
+        /// <response code="404">Task not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpDelete("Running/{TaskID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -132,7 +139,9 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="taskId">Task Id.</param>
         /// <param name="triggerInfos">Triggers.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Task triggers updated.</response>
+        /// <response code="404">Task not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("{TaskID}/Triggers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]

From 7516e3ebbec82b732e8e4355ae108e7030e1e00e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 17:12:56 -0600
Subject: [PATCH 089/463] Update endpoint docs

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index b0cdfb86e9..30fb951cf9 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -41,7 +41,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoId">Video ID.</param>
         /// <param name="mediaSourceId">Media Source ID.</param>
         /// <param name="index">Attachment Index.</param>
-        /// <returns>Attachment.</returns>
+        /// <response code="200">Attachment retrieved.</response>
+        /// <response code="404">Video or attachment not found.</response>
+        /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
         [Produces("application/octet-stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 25002483a3fd7f9d1c79c74338ac18c8eabfb0ed Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 17:23:02 -0600
Subject: [PATCH 090/463] Update endpoint docs

---
 Jellyfin.Api/Controllers/DevicesController.cs | 53 ++++++++++++++++---
 1 file changed, 45 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index cebb51ccfe..02cf1bc446 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,6 +1,7 @@
 #nullable enable
 
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
@@ -46,11 +47,12 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="supportsSync">/// Gets or sets a value indicating whether [supports synchronize].</param>
         /// <param name="userId">/// Gets or sets the user identifier.</param>
-        /// <returns>Device Infos.</returns>
+        /// <response code="200">Devices retrieved.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
         [HttpGet]
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<DeviceInfo[]> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        public ActionResult<IEnumerable<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
             var devices = _deviceManager.GetDevices(deviceQuery);
@@ -61,7 +63,9 @@ namespace Jellyfin.Api.Controllers
         /// Get info for a device.
         /// </summary>
         /// <param name="id">Device Id.</param>
-        /// <returns>Device Info.</returns>
+        /// <response code="200">Device info retrieved.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpGet("Info")]
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -81,7 +85,9 @@ namespace Jellyfin.Api.Controllers
         /// Get options for a device.
         /// </summary>
         /// <param name="id">Device Id.</param>
-        /// <returns>Device Info.</returns>
+        /// <response code="200">Device options retrieved.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpGet("Options")]
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -102,7 +108,9 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="id">Device Id.</param>
         /// <param name="deviceOptions">Device Options.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Device options updated.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpPost("Options")]
         [Authenticated(Roles = "Admin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -125,11 +133,19 @@ namespace Jellyfin.Api.Controllers
         /// Deletes a device.
         /// </summary>
         /// <param name="id">Device Id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Device deleted.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
         {
+            var existingDevice = _deviceManager.GetDevice(id);
+            if (existingDevice == null)
+            {
+                return NotFound();
+            }
+
             var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
 
             foreach (var session in sessions)
@@ -144,11 +160,19 @@ namespace Jellyfin.Api.Controllers
         /// Gets camera upload history for a device.
         /// </summary>
         /// <param name="id">Device Id.</param>
-        /// <returns>Content Upload History.</returns>
+        /// <response code="200">Device upload history retrieved.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the device upload history on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpGet("CameraUploads")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ContentUploadHistory> GetCameraUploads([FromQuery, BindRequired] string id)
         {
+            var existingDevice = _deviceManager.GetDevice(id);
+            if (existingDevice == null)
+            {
+                return NotFound();
+            }
+
             var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
             return uploadHistory;
         }
@@ -160,7 +184,14 @@ namespace Jellyfin.Api.Controllers
         /// <param name="album">Album.</param>
         /// <param name="name">Name.</param>
         /// <param name="id">Id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Contents uploaded.</response>
+        /// <response code="400">No uploaded contents.</response>
+        /// <response code="404">Device not found.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> on success,
+        /// or a <see cref="NotFoundResult"/> if the device could not be found
+        /// or a <see cref="BadRequestResult"/> if the upload contains no files.
+        /// </returns>
         [HttpPost("CameraUploads")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -170,6 +201,12 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, BindRequired] string name,
             [FromQuery, BindRequired] string id)
         {
+            var existingDevice = _deviceManager.GetDevice(id);
+            if (existingDevice == null)
+            {
+                return NotFound();
+            }
+
             Stream fileStream;
             string contentType;
 

From cbd4a64e670eda1c30be6000a8f6cceccc93ddfa Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 18:46:27 -0600
Subject: [PATCH 091/463] Update endpoint docs

---
 .../Images/ImageByNameController.cs           | 27 ++++++++++++-------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index ce509b4e6d..6160d54028 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -42,10 +42,11 @@ namespace Jellyfin.Api.Controllers.Images
         /// <summary>
         ///     Get all general images.
         /// </summary>
-        /// <returns>General images.</returns>
+        /// <response code="200">Retrieved list of images.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("General")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<ImageByNameInfo[]> GetGeneralImages()
+        public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
         {
             return Ok(GetImageList(_applicationPaths.GeneralPath, false));
         }
@@ -55,7 +56,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <param name="name">The name of the image.</param>
         /// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
-        /// <returns>Image Stream.</returns>
+        /// <response code="200">Image stream retrieved.</response>
+        /// <response code="404">Image not found.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("General/{Name}/{Type}")]
         [Produces("application/octet-stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -82,10 +85,11 @@ namespace Jellyfin.Api.Controllers.Images
         /// <summary>
         ///     Get all general images.
         /// </summary>
-        /// <returns>General images.</returns>
+        /// <response code="200">Retrieved list of images.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("Ratings")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<ImageByNameInfo[]> GetRatingImages()
+        public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
         {
             return Ok(GetImageList(_applicationPaths.RatingsPath, false));
         }
@@ -95,7 +99,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <param name="theme">The theme to get the image from.</param>
         /// <param name="name">The name of the image.</param>
-        /// <returns>Image Stream.</returns>
+        /// <response code="200">Image stream retrieved.</response>
+        /// <response code="404">Image not found.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("Ratings/{Theme}/{Name}")]
         [Produces("application/octet-stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -110,7 +116,8 @@ namespace Jellyfin.Api.Controllers.Images
         /// <summary>
         ///     Get all media info images.
         /// </summary>
-        /// <returns>Media Info images.</returns>
+        /// <response code="200">Image list retrieved.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("MediaInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ImageByNameInfo[]> GetMediaInfoImages()
@@ -123,7 +130,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// </summary>
         /// <param name="theme">The theme to get the image from.</param>
         /// <param name="name">The name of the image.</param>
-        /// <returns>Image Stream.</returns>
+        /// <response code="200">Image stream retrieved.</response>
+        /// <response code="404">Image not found.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("MediaInfo/{Theme}/{Name}")]
         [Produces("application/octet-stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -141,7 +150,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="basePath">Path to begin search.</param>
         /// <param name="theme">Theme to search.</param>
         /// <param name="name">File name to search for.</param>
-        /// <returns>Image Stream.</returns>
+        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name)
         {
             var themeFolder = Path.Combine(basePath, theme);

From 35dbcea9311589ea7b9a10ab02da557a2bfb46fc Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 18:47:05 -0600
Subject: [PATCH 092/463] Return array -> ienumerable

---
 Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index 6160d54028..67ebaa4e09 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("MediaInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<ImageByNameInfo[]> GetMediaInfoImages()
+        public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
         {
             return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
         }

From d7d8118b42c8abc8a4f12c4f2b0fb97cc6384ba7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 3 May 2020 14:02:15 -0600
Subject: [PATCH 093/463] Fix xml docs

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 3cbb3a3a3f..8d82ca10f1 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets notification services.
         /// </summary>
-        /// <response>All notification services returned.</response>
+        /// <response code="200">All notification services returned.</response>
         /// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
         [HttpGet("Services")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 2b1b9a64b6b7a3bac4d96642cda7a0c55d5cae74 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 8 May 2020 08:40:37 -0600
Subject: [PATCH 094/463] Add OperationId to SwaggerGen

---
 .../Extensions/ApiServiceCollectionExtensions.cs            | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index a354f45aad..344ef6a5ff 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Reflection;
 using System.Text.Json.Serialization;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
@@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
 
 namespace Jellyfin.Server.Extensions
 {
@@ -112,6 +114,10 @@ namespace Jellyfin.Server.Extensions
                 // Order actions by route path, then by http method.
                 c.OrderActionsBy(description =>
                     $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}");
+
+                // Use method name as operationId
+                c.CustomOperationIds(description =>
+                    description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
             });
         }
     }

From 37f55b5c217db5343ab196094f67dc84e71d4ef0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 17 May 2020 19:56:02 -0600
Subject: [PATCH 095/463] apply doc suggestions

---
 Jellyfin.Api/Controllers/StartupController.cs | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 66e4774aa0..ed1dc1ede3 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -34,7 +34,7 @@ namespace Jellyfin.Api.Controllers
         /// Completes the startup wizard.
         /// </summary>
         /// <response code="200">Startup wizard completed.</response>
-        /// <returns>Status.</returns>
+        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
         [HttpPost("Complete")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult CompleteWizard()
@@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         /// Gets the initial startup wizard configuration.
         /// </summary>
         /// <response code="200">Initial startup wizard configuration retrieved.</response>
-        /// <returns>The initial startup wizard configuration.</returns>
+        /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
         [HttpGet("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="metadataCountryCode">The metadata country code.</param>
         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
         /// <response code="200">Configuration saved.</response>
-        /// <returns>Status.</returns>
+        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
         [HttpPost("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult UpdateInitialConfiguration(
@@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableRemoteAccess">Enable remote access.</param>
         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
         /// <response code="200">Configuration saved.</response>
-        /// <returns>Status.</returns>
+        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
         [HttpPost("RemoteAccess")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
@@ -121,7 +121,10 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="startupUserDto">The DTO containing username and password.</param>
         /// <response code="200">Updated user name and password.</response>
-        /// <returns>The async task.</returns>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous update operation.
+        /// The task result contains an <see cref="OkResult"/> indicating success.
+        /// </returns>
         [HttpPost("User")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)

From c4f8ba55f2b3424be4a6ff1044d13327fe36b687 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 08:28:02 -0600
Subject: [PATCH 096/463] Rename to AttachmentsController ->
 VideoAttachmentsController

---
 ...tachmentsController.cs => VideoAttachmentsController.cs} | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
 rename Jellyfin.Api/Controllers/{AttachmentsController.cs => VideoAttachmentsController.cs} (94%)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
similarity index 94%
rename from Jellyfin.Api/Controllers/AttachmentsController.cs
rename to Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 30fb951cf9..69e8473735 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -17,17 +17,17 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     [Route("Videos")]
     [Authorize]
-    public class AttachmentsController : Controller
+    public class VideoAttachmentsController : Controller
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IAttachmentExtractor _attachmentExtractor;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="AttachmentsController"/> class.
+        /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
         /// </summary>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
-        public AttachmentsController(
+        public VideoAttachmentsController(
             ILibraryManager libraryManager,
             IAttachmentExtractor attachmentExtractor)
         {

From 45d750f10657da8f7914999098ecffcdbfedbd2d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 17:36:05 -0600
Subject: [PATCH 097/463] Move AttachmentsService to AttachmentsController

---
 .../Controllers/AttachmentsController.cs      | 86 +++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
new file mode 100644
index 0000000000..5d48a79b9b
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Attachments controller.
+    /// </summary>
+    [Route("Videos")]
+    [Authenticated]
+    public class AttachmentsController : Controller
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IAttachmentExtractor _attachmentExtractor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AttachmentsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+        public AttachmentsController(
+            ILibraryManager libraryManager,
+            IAttachmentExtractor attachmentExtractor)
+        {
+            _libraryManager = libraryManager;
+            _attachmentExtractor = attachmentExtractor;
+        }
+
+        /// <summary>
+        /// Get video attachment.
+        /// </summary>
+        /// <param name="videoId">Video ID.</param>
+        /// <param name="mediaSourceId">Media Source ID.</param>
+        /// <param name="index">Attachment Index.</param>
+        /// <returns>Attachment.</returns>
+        [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
+        [Produces("application/octet-stream")]
+        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
+        public async Task<IActionResult> GetAttachment(
+            [FromRoute] Guid videoId,
+            [FromRoute] string mediaSourceId,
+            [FromRoute] int index)
+        {
+            try
+            {
+                var item = _libraryManager.GetItemById(videoId);
+                if (item == null)
+                {
+                    return NotFound();
+                }
+
+                var (attachment, stream) = await _attachmentExtractor.GetAttachment(
+                        item,
+                        mediaSourceId,
+                        index,
+                        CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                var contentType = "application/octet-stream";
+                if (string.IsNullOrWhiteSpace(attachment.MimeType))
+                {
+                    contentType = attachment.MimeType;
+                }
+
+                return new FileStreamResult(stream, contentType);
+            }
+            catch (ResourceNotFoundException e)
+            {
+                return StatusCode(StatusCodes.Status404NotFound, e.Message);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
+            }
+        }
+    }
+}

From 8eac528815bc7ab673b361f48b90b3b28ccbc070 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 19 Apr 2020 17:37:15 -0600
Subject: [PATCH 098/463] nullable

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index 5d48a79b9b..f4c1a761fb 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Threading;
 using System.Threading.Tasks;

From 84fcb4926ccce968a920bed7324ef6f037c4f5e1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 07:52:33 -0600
Subject: [PATCH 099/463] Remove exception handler

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index f4c1a761fb..aeeaf5cbdc 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -79,10 +79,6 @@ namespace Jellyfin.Api.Controllers
             {
                 return StatusCode(StatusCodes.Status404NotFound, e.Message);
             }
-            catch (Exception e)
-            {
-                return StatusCode(StatusCodes.Status500InternalServerError, e.Message);
-            }
         }
     }
 }

From 15e9fbb923b8aa91692cd9c8c68ec7dde638c1e2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Apr 2020 13:57:11 -0600
Subject: [PATCH 100/463] move to ActionResult<T>

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index aeeaf5cbdc..351401de18 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -44,10 +44,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Attachment.</returns>
         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
         [Produces("application/octet-stream")]
-        [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
-        public async Task<IActionResult> GetAttachment(
+        public async Task<ActionResult<FileStreamResult>> GetAttachment(
             [FromRoute] Guid videoId,
             [FromRoute] string mediaSourceId,
             [FromRoute] int index)

From 177339e8d5f3ad9eea6a3d6cd068e58d637e443d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 23 Apr 2020 10:04:37 -0600
Subject: [PATCH 101/463] Fix Authorize attributes

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index 351401de18..b0cdfb86e9 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authenticated]
+    [Authorize]
     public class AttachmentsController : Controller
     {
         private readonly ILibraryManager _libraryManager;

From 26a2bea179b8c2d8b772b714e6296c03b5c1e0d3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 2 May 2020 17:12:56 -0600
Subject: [PATCH 102/463] Update endpoint docs

---
 Jellyfin.Api/Controllers/AttachmentsController.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
index b0cdfb86e9..30fb951cf9 100644
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/AttachmentsController.cs
@@ -41,7 +41,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoId">Video ID.</param>
         /// <param name="mediaSourceId">Media Source ID.</param>
         /// <param name="index">Attachment Index.</param>
-        /// <returns>Attachment.</returns>
+        /// <response code="200">Attachment retrieved.</response>
+        /// <response code="404">Video or attachment not found.</response>
+        /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
         [Produces("application/octet-stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From a7a725173da0be952e0a7407f9f42f1ea1123f84 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 08:28:02 -0600
Subject: [PATCH 103/463] Rename to AttachmentsController ->
 VideoAttachmentsController

---
 .../Controllers/AttachmentsController.cs      | 85 -------------------
 1 file changed, 85 deletions(-)
 delete mode 100644 Jellyfin.Api/Controllers/AttachmentsController.cs

diff --git a/Jellyfin.Api/Controllers/AttachmentsController.cs b/Jellyfin.Api/Controllers/AttachmentsController.cs
deleted file mode 100644
index 30fb951cf9..0000000000
--- a/Jellyfin.Api/Controllers/AttachmentsController.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-#nullable enable
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers
-{
-    /// <summary>
-    /// Attachments controller.
-    /// </summary>
-    [Route("Videos")]
-    [Authorize]
-    public class AttachmentsController : Controller
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IAttachmentExtractor _attachmentExtractor;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AttachmentsController"/> class.
-        /// </summary>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
-        public AttachmentsController(
-            ILibraryManager libraryManager,
-            IAttachmentExtractor attachmentExtractor)
-        {
-            _libraryManager = libraryManager;
-            _attachmentExtractor = attachmentExtractor;
-        }
-
-        /// <summary>
-        /// Get video attachment.
-        /// </summary>
-        /// <param name="videoId">Video ID.</param>
-        /// <param name="mediaSourceId">Media Source ID.</param>
-        /// <param name="index">Attachment Index.</param>
-        /// <response code="200">Attachment retrieved.</response>
-        /// <response code="404">Video or attachment not found.</response>
-        /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
-        [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
-        [Produces("application/octet-stream")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public async Task<ActionResult<FileStreamResult>> GetAttachment(
-            [FromRoute] Guid videoId,
-            [FromRoute] string mediaSourceId,
-            [FromRoute] int index)
-        {
-            try
-            {
-                var item = _libraryManager.GetItemById(videoId);
-                if (item == null)
-                {
-                    return NotFound();
-                }
-
-                var (attachment, stream) = await _attachmentExtractor.GetAttachment(
-                        item,
-                        mediaSourceId,
-                        index,
-                        CancellationToken.None)
-                    .ConfigureAwait(false);
-
-                var contentType = "application/octet-stream";
-                if (string.IsNullOrWhiteSpace(attachment.MimeType))
-                {
-                    contentType = attachment.MimeType;
-                }
-
-                return new FileStreamResult(stream, contentType);
-            }
-            catch (ResourceNotFoundException e)
-            {
-                return StatusCode(StatusCodes.Status404NotFound, e.Message);
-            }
-        }
-    }
-}

From 1c471d58551043dab3c808952d9834163cac3078 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:01:00 -0600
Subject: [PATCH 104/463] Clean UpdateDisplayPreferences endpoint

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 2837ea8e87..579b5df5d4 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -74,14 +74,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, BindRequired] string client,
             [FromBody, BindRequired] DisplayPreferences displayPreferences)
         {
-            if (!ModelState.IsValid)
-            {
-                return BadRequest(ModelState);
-            }
-
             if (displayPreferencesId == null)
             {
-                // do nothing.
+                // TODO - refactor so parameter doesn't exist or is actually used.
             }
 
             _displayPreferencesRepository.SaveDisplayPreferences(

From c998935d29d04a55babdeb0adcf1d1091611b1e3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:06:37 -0600
Subject: [PATCH 105/463] Apply review suggestions

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index ad70bf83b2..3e3359ec77 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -3,8 +3,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using MediaBrowser.Controller.Net;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Model.Tasks;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -14,7 +15,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Scheduled Tasks Controller.
     /// </summary>
-    // [Authenticated]
+    [Authorize(Policy = Policies.RequiresElevation)]
     public class ScheduledTasksController : BaseJellyfinApiController
     {
         private readonly ITaskManager _taskManager;
@@ -82,8 +83,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            var result = ScheduledTaskHelpers.GetTaskInfo(task);
-            return Ok(result);
+            return ScheduledTaskHelpers.GetTaskInfo(task);
         }
 
         /// <summary>

From 98bd61e36443452a280dc9d3543baecc10b561ed Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:14:37 -0600
Subject: [PATCH 106/463] Clean up routes

---
 Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index 67ebaa4e09..62fcb5a2a6 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
         {
-            return Ok(GetImageList(_applicationPaths.GeneralPath, false));
+            return GetImageList(_applicationPaths.GeneralPath, false);
         }
 
         /// <summary>
@@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
         {
-            return Ok(GetImageList(_applicationPaths.RatingsPath, false));
+            return GetImageList(_applicationPaths.RatingsPath, false);
         }
 
         /// <summary>
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers.Images
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
         {
-            return Ok(GetImageList(_applicationPaths.MediaInfoImagesPath, false));
+            return GetImageList(_applicationPaths.MediaInfoImagesPath, false);
         }
 
         /// <summary>

From 2923013c6ed0c5c4e7325893be0822d8fcd9de47 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:23:28 -0600
Subject: [PATCH 107/463] Clean Remote Image Controller.

---
 .../Images/RemoteImageController.cs           | 38 +++++++++++--------
 1 file changed, 23 insertions(+), 15 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index a0754ed4eb..665db561bf 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -1,6 +1,7 @@
 #nullable enable
 
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -9,12 +10,12 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers.Images
     /// Remote Images Controller.
     /// </summary>
     [Route("Images")]
-    [Authenticated]
+    [Authorize]
     public class RemoteImageController : BaseJellyfinApiController
     {
         private readonly IProviderManager _providerManager;
@@ -60,7 +61,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="providerName">Optional. The image provider to use.</param>
-        /// <param name="includeAllLanguages">Optinal. Include all languages.</param>
+        /// <param name="includeAllLanguages">Optional. Include all languages.</param>
+        /// <response code="200">Remote Images returned.</response>
+        /// <response code="404">Item not found.</response>
         /// <returns>Remote Image Result.</returns>
         [HttpGet("{Id}/RemoteImages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -116,18 +119,20 @@ namespace Jellyfin.Api.Controllers.Images
             }
 
             result.Images = imageArray;
-            return Ok(result);
+            return result;
         }
 
         /// <summary>
         /// Gets available remote image providers for an item.
         /// </summary>
         /// <param name="id">Item Id.</param>
-        /// <returns>List of providers.</returns>
+        /// <response code="200">Returned remote image providers.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>List of remote image providers.</returns>
         [HttpGet("{Id}/RemoteImages/Providers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<ImageProviderInfo[]> GetRemoteImageProviders([FromRoute] string id)
+        public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] string id)
         {
             var item = _libraryManager.GetItemById(id);
             if (item == null)
@@ -135,14 +140,15 @@ namespace Jellyfin.Api.Controllers.Images
                 return NotFound();
             }
 
-            var providers = _providerManager.GetRemoteImageProviderInfo(item);
-            return Ok(providers);
+            return Ok(_providerManager.GetRemoteImageProviderInfo(item));
         }
 
         /// <summary>
         /// Gets a remote image.
         /// </summary>
         /// <param name="imageUrl">The image url.</param>
+        /// <response code="200">Remote image returned.</response>
+        /// <response code="404">Remote image not found.</response>
         /// <returns>Image Stream.</returns>
         [HttpGet("Remote")]
         [Produces("application/octet-stream")]
@@ -154,7 +160,7 @@ namespace Jellyfin.Api.Controllers.Images
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
 
             string? contentPath = null;
-            bool hasFile = false;
+            var hasFile = false;
 
             try
             {
@@ -166,11 +172,11 @@ namespace Jellyfin.Api.Controllers.Images
             }
             catch (FileNotFoundException)
             {
-                // Means the file isn't cached yet
+                // The file isn't cached yet
             }
             catch (IOException)
             {
-                // Means the file isn't cached yet
+                // The file isn't cached yet
             }
 
             if (!hasFile)
@@ -194,7 +200,9 @@ namespace Jellyfin.Api.Controllers.Images
         /// <param name="id">Item Id.</param>
         /// <param name="type">The image type.</param>
         /// <param name="imageUrl">The image url.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Remote image downloaded.</response>
+        /// <response code="404">Remote image not found.</response>
+        /// <returns>Download status.</returns>
         [HttpPost("{Id}/RemoteImages/Download")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -245,10 +253,10 @@ namespace Jellyfin.Api.Controllers.Images
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            using (var stream = result.Content)
+            await using (var stream = result.Content)
             {
-                using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-                await stream.CopyToAsync(filestream).ConfigureAwait(false);
+                await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+                await stream.CopyToAsync(fileStream).ConfigureAwait(false);
             }
 
             Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));

From 839de72f9a320bfe09cdd9c2fcab6806f3106916 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:24:04 -0600
Subject: [PATCH 108/463] Fix authentication attribute

---
 Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index 62fcb5a2a6..dadb344385 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -7,10 +7,10 @@ using System.Linq;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers.Images
     ///     Images By Name Controller.
     /// </summary>
     [Route("Images")]
-    [Authenticated]
+    [Authorize]
     public class ImageByNameController : BaseJellyfinApiController
     {
         private readonly IServerApplicationPaths _applicationPaths;

From 6dbbfcbfbef28e8866aa0144170e1edfff1a2bcb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:29:59 -0600
Subject: [PATCH 109/463] update xml docs

---
 Jellyfin.Api/Controllers/ConfigurationController.cs | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index b508ac0547..992cb00874 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets application configuration.
         /// </summary>
+        /// <response code="200">Application configuration returned.</response>
         /// <returns>Application configuration.</returns>
         [HttpGet("Configuration")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -56,7 +57,8 @@ namespace Jellyfin.Api.Controllers
         /// Updates application configuration.
         /// </summary>
         /// <param name="configuration">Configuration.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Configuration updated.</response>
+        /// <returns>Update status.</returns>
         [HttpPost("Configuration")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -70,6 +72,7 @@ namespace Jellyfin.Api.Controllers
         /// Gets a named configuration.
         /// </summary>
         /// <param name="key">Configuration key.</param>
+        /// <response code="200">Configuration returned.</response>
         /// <returns>Configuration.</returns>
         [HttpGet("Configuration/{Key}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -82,7 +85,8 @@ namespace Jellyfin.Api.Controllers
         /// Updates named configuration.
         /// </summary>
         /// <param name="key">Configuration key.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Named configuration updated.</response>
+        /// <returns>Update status.</returns>
         [HttpPost("Configuration/{Key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -103,7 +107,8 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a default MetadataOptions object.
         /// </summary>
-        /// <returns>MetadataOptions.</returns>
+        /// <response code="200">Metadata options returned.</response>
+        /// <returns>Default MetadataOptions.</returns>
         [HttpGet("Configuration/MetadataOptions/Default")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -116,6 +121,7 @@ namespace Jellyfin.Api.Controllers
         /// Updates the path to the media encoder.
         /// </summary>
         /// <param name="mediaEncoderPath">Media encoder path form body.</param>
+        /// <response code="200">Media encoder path updated.</response>
         /// <returns>Status.</returns>
         [HttpPost("MediaEncoder/Path")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]

From 6c376f18f7f094d41a0d3e5387ce83e5b0b66c4a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 09:47:02 -0600
Subject: [PATCH 110/463] update xml docs

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 4e2621b7b3..733f1e6d86 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -21,6 +22,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Channels Controller.
     /// </summary>
+    [Authorize]
     public class ChannelsController : BaseJellyfinApiController
     {
         private readonly IChannelManager _channelManager;
@@ -46,6 +48,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
         /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
         /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
+        /// <response code="200">Channels returned.</response>
         /// <returns>Channels.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -71,6 +74,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get all channel features.
         /// </summary>
+        /// <response code="200">All channel features returned.</response>
         /// <returns>Channel features.</returns>
         [HttpGet("Features")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -83,6 +87,7 @@ namespace Jellyfin.Api.Controllers
         /// Get channel features.
         /// </summary>
         /// <param name="id">Channel id.</param>
+        /// <response code="200">Channel features returned.</response>
         /// <returns>Channel features.</returns>
         [HttpGet("{Id}/Features")]
         public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string id)
@@ -102,6 +107,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <response code="200">Channel items returned.</response>
         /// <returns>Channel items.</returns>
         [HttpGet("{Id}/Items")]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
@@ -175,6 +181,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
+        /// <response code="200">Latest channel items returned.</response>
         /// <returns>Latest channel items.</returns>
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
             [FromQuery] Guid? userId,

From e03c97d7cdfad65a48bc0aff6ca0e45f9b3ec3cd Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 10:02:52 -0600
Subject: [PATCH 111/463] update xml docs

---
 Jellyfin.Api/Controllers/EnvironmentController.cs | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 139c1af083..78c206ba19 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -40,7 +40,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="path">The path.</param>
         /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
         /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
-        /// <returns>File system entries.</returns>
+        /// <response code="200">Directory contents returned.</response>
+        /// <returns>Directory contents.</returns>
         [HttpGet("DirectoryContents")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
@@ -79,7 +80,9 @@ namespace Jellyfin.Api.Controllers
         /// Validates path.
         /// </summary>
         /// <param name="validatePathDto">Validate request object.</param>
-        /// <returns>Status.</returns>
+        /// <response code="200">Path validated.</response>
+        /// <response code="404">Path not found.</response>
+        /// <returns>Validation status.</returns>
         [HttpPost("ValidatePath")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -132,6 +135,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets network paths.
         /// </summary>
+        /// <response code="200">Empty array returned.</response>
         /// <returns>List of entries.</returns>
         [Obsolete("This endpoint is obsolete.")]
         [HttpGet("NetworkShares")]
@@ -144,6 +148,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available drives from the server's file system.
         /// </summary>
+        /// <response code="200">List of entries returned.</response>
         /// <returns>List of entries.</returns>
         [HttpGet("Drives")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -189,6 +194,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get Default directory browser.
         /// </summary>
+        /// <response code="200">Default directory browser returned.</response>
         /// <returns>Default directory browser.</returns>
         [HttpGet("DefaultDirectoryBrowser")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From a11a1934399b8cbce0487ced49d2f8e7065b436a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 10:04:09 -0600
Subject: [PATCH 112/463] Remove CameraUpload endpoints

---
 Jellyfin.Api/Controllers/DevicesController.cs | 83 -------------------
 1 file changed, 83 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 02cf1bc446..64dc2322dd 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -2,9 +2,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
@@ -155,85 +152,5 @@ namespace Jellyfin.Api.Controllers
 
             return Ok();
         }
-
-        /// <summary>
-        /// Gets camera upload history for a device.
-        /// </summary>
-        /// <param name="id">Device Id.</param>
-        /// <response code="200">Device upload history retrieved.</response>
-        /// <response code="404">Device not found.</response>
-        /// <returns>An <see cref="OkResult"/> containing the device upload history on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
-        [HttpGet("CameraUploads")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<ContentUploadHistory> GetCameraUploads([FromQuery, BindRequired] string id)
-        {
-            var existingDevice = _deviceManager.GetDevice(id);
-            if (existingDevice == null)
-            {
-                return NotFound();
-            }
-
-            var uploadHistory = _deviceManager.GetCameraUploadHistory(id);
-            return uploadHistory;
-        }
-
-        /// <summary>
-        /// Uploads content.
-        /// </summary>
-        /// <param name="deviceId">Device Id.</param>
-        /// <param name="album">Album.</param>
-        /// <param name="name">Name.</param>
-        /// <param name="id">Id.</param>
-        /// <response code="200">Contents uploaded.</response>
-        /// <response code="400">No uploaded contents.</response>
-        /// <response code="404">Device not found.</response>
-        /// <returns>
-        /// An <see cref="OkResult"/> on success,
-        /// or a <see cref="NotFoundResult"/> if the device could not be found
-        /// or a <see cref="BadRequestResult"/> if the upload contains no files.
-        /// </returns>
-        [HttpPost("CameraUploads")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status400BadRequest)]
-        public async Task<ActionResult> PostCameraUploadAsync(
-            [FromQuery, BindRequired] string deviceId,
-            [FromQuery, BindRequired] string album,
-            [FromQuery, BindRequired] string name,
-            [FromQuery, BindRequired] string id)
-        {
-            var existingDevice = _deviceManager.GetDevice(id);
-            if (existingDevice == null)
-            {
-                return NotFound();
-            }
-
-            Stream fileStream;
-            string contentType;
-
-            if (Request.HasFormContentType)
-            {
-                if (Request.Form.Files.Any())
-                {
-                    fileStream = Request.Form.Files[0].OpenReadStream();
-                    contentType = Request.Form.Files[0].ContentType;
-                }
-                else
-                {
-                    return BadRequest();
-                }
-            }
-            else
-            {
-                fileStream = Request.Body;
-                contentType = Request.ContentType;
-            }
-
-            await _deviceManager.AcceptCameraUpload(
-                deviceId,
-                fileStream,
-                new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false);
-
-            return Ok();
-        }
     }
 }

From cf78edc979b626ff11ff88889f618cba50c5ee5f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 10:05:23 -0600
Subject: [PATCH 113/463] Fix Authorize attributes

---
 Jellyfin.Api/Controllers/DevicesController.cs | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 64dc2322dd..b22b5f985b 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -2,11 +2,13 @@
 
 using System;
 using System.Collections.Generic;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Devices;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -16,7 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Devices Controller.
     /// </summary>
-    [Authenticated]
+    [Authorize]
     public class DevicesController : BaseJellyfinApiController
     {
         private readonly IDeviceManager _deviceManager;
@@ -47,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Devices retrieved.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
         [HttpGet]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
@@ -64,7 +66,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Device not found.</response>
         /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpGet("Info")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string id)
@@ -86,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Device not found.</response>
         /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpGet("Options")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string id)
@@ -109,7 +111,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Device not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpPost("Options")]
-        [Authenticated(Roles = "Admin")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateDeviceOptions(

From cdb25e355c6ebf9ba09f44a7bb7e35286e50976e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 10:06:25 -0600
Subject: [PATCH 114/463] Fix return value

---
 Jellyfin.Api/Controllers/DevicesController.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index b22b5f985b..a46d3f9370 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -51,11 +52,10 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
-            var devices = _deviceManager.GetDevices(deviceQuery);
-            return Ok(devices);
+            return _deviceManager.GetDevices(deviceQuery);
         }
 
         /// <summary>

From 24543b04c110b7cdf275c314ac61065dc36b25e8 Mon Sep 17 00:00:00 2001
From: Bruce <bruce.coelho93@gmail.com>
Date: Tue, 19 May 2020 18:13:42 +0100
Subject: [PATCH 115/463] Applying review suggestion to documentation

---
 Jellyfin.Api/Controllers/PackageController.cs | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index b5ee47ee43..f37319c19e 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -1,4 +1,5 @@
 #nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -32,10 +33,10 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Gets a package by name or assembly guid.
+        /// Gets a package by name or assembly GUID.
         /// </summary>
         /// <param name="name">The name of the package.</param>
-        /// <param name="assemblyGuid">The guid of the associated assembly.</param>
+        /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
         [HttpGet("/{Name}")]
         [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
@@ -69,7 +70,7 @@ namespace Jellyfin.Api.Controllers
         /// Installs a package.
         /// </summary>
         /// <param name="name">Package name.</param>
-        /// <param name="assemblyGuid">Guid of the associated assembly.</param>
+        /// <param name="assemblyGuid">GUID of the associated assembly.</param>
         /// <param name="version">Optional version. Defaults to latest version.</param>
         /// <response code="200">Package found.</response>
         /// <response code="404">Package not found.</response>

From 2f2bceb1104d8ea669ca21fc40200247aca956ed Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 12:56:57 -0600
Subject: [PATCH 116/463] Remove default parameter values

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 3e3359ec77..19cce974ea 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -39,8 +39,8 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<IScheduledTaskWorker> GetTasks(
-            [FromQuery] bool? isHidden = false,
-            [FromQuery] bool? isEnabled = false)
+            [FromQuery] bool? isHidden,
+            [FromQuery] bool? isEnabled)
         {
             IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
 

From b28dd47a0fc5b18111678acede335474f9007b8f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 12:58:09 -0600
Subject: [PATCH 117/463] implement review suggestions

---
 Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 69e8473735..a10dd40593 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
             }
             catch (ResourceNotFoundException e)
             {
-                return StatusCode(StatusCodes.Status404NotFound, e.Message);
+                return NotFound(e.Message);
             }
         }
     }

From 51d54a8ca40f987bce877ad1d7dc78b1cb26b8a3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 13:02:02 -0600
Subject: [PATCH 118/463] Fix return content type

---
 .../Controllers/VideoAttachmentsController.cs      | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index a10dd40593..596d211900 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -1,11 +1,13 @@
 #nullable enable
 
 using System;
+using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -17,7 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     [Route("Videos")]
     [Authorize]
-    public class VideoAttachmentsController : Controller
+    public class VideoAttachmentsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IAttachmentExtractor _attachmentExtractor;
@@ -45,7 +47,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Video or attachment not found.</response>
         /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
-        [Produces("application/octet-stream")]
+        [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<FileStreamResult>> GetAttachment(
@@ -68,11 +70,9 @@ namespace Jellyfin.Api.Controllers
                         CancellationToken.None)
                     .ConfigureAwait(false);
 
-                var contentType = "application/octet-stream";
-                if (string.IsNullOrWhiteSpace(attachment.MimeType))
-                {
-                    contentType = attachment.MimeType;
-                }
+                var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
+                    ? MediaTypeNames.Application.Octet
+                    : attachment.MimeType;
 
                 return new FileStreamResult(stream, contentType);
             }

From 2689865858c779491c98066df3f1e7d894f7c3b8 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 13:02:35 -0600
Subject: [PATCH 119/463] Remove unused using

---
 Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 596d211900..86d9322fe4 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -7,7 +7,6 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;

From 070edd9e5bff63d3f158b6ca8b37095adc686492 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 13:13:07 -0600
Subject: [PATCH 120/463] Fix MediaType usage

---
 Jellyfin.Api/Controllers/Images/ImageByNameController.cs | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index dadb344385..fa60809773 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net.Mime;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -60,7 +61,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("General/{Name}/{Type}")]
-        [Produces("application/octet-stream")]
+        [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type)
@@ -103,7 +104,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("Ratings/{Theme}/{Name}")]
-        [Produces("application/octet-stream")]
+        [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetRatingImage(
@@ -134,7 +135,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("MediaInfo/{Theme}/{Name}")]
-        [Produces("application/octet-stream")]
+        [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetMediaInfoImage(

From 5f0c37d5745cbf2632d377905a0763f0254bca08 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 19 May 2020 13:22:09 -0600
Subject: [PATCH 121/463] Fix DefaultDirectoryBrowserInfo naming

---
 Jellyfin.Api/Controllers/EnvironmentController.cs             | 4 ++--
 ...ectoryBrowserInfo.cs => DefaultDirectoryBrowserInfoDto.cs} | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)
 rename Jellyfin.Api/Models/EnvironmentDtos/{DefaultDirectoryBrowserInfo.cs => DefaultDirectoryBrowserInfoDto.cs} (84%)

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 78c206ba19..8d9d2642f4 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -198,9 +198,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Default directory browser.</returns>
         [HttpGet("DefaultDirectoryBrowser")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<DefaultDirectoryBrowserInfo> GetDefaultDirectoryBrowser()
+        public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
         {
-            return new DefaultDirectoryBrowserInfo();
+            return new DefaultDirectoryBrowserInfoDto();
         }
     }
 }
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
similarity index 84%
rename from Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs
rename to Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
index 6b1c750bf6..a86815b81c 100644
--- a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfo.cs
+++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
@@ -3,7 +3,7 @@ namespace Jellyfin.Api.Models.EnvironmentDtos
     /// <summary>
     /// Default directory browser info.
     /// </summary>
-    public class DefaultDirectoryBrowserInfo
+    public class DefaultDirectoryBrowserInfoDto
     {
         /// <summary>
         /// Gets or sets the path.

From fb068b76a12bf9de5de75a0b8079effcd0336ecf Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 20 May 2020 07:18:51 -0600
Subject: [PATCH 122/463] Use correct MediaTypeName

---
 Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index 665db561bf..1155cc653e 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Remote image not found.</response>
         /// <returns>Image Stream.</returns>
         [HttpGet("Remote")]
-        [Produces("application/octet-stream")]
+        [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] string imageUrl)

From 341b947cdecdfc791c1bc3e72da1e68cd3754c3a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 22 May 2020 10:48:01 -0600
Subject: [PATCH 123/463] Move int64 converter to JsonDefaults location

---
 .../Extensions/ApiServiceCollectionExtensions.cs       |  2 --
 .../Json/Converters/JsonInt64Converter.cs              | 10 +++++-----
 MediaBrowser.Common/Json/JsonDefaults.cs               |  1 +
 3 files changed, 6 insertions(+), 7 deletions(-)
 rename Jellyfin.Server/Converters/LongToStringConverter.cs => MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs (85%)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index afd42ac5ac..71ef9a69a2 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -4,7 +4,6 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
-using Jellyfin.Server.Converters;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -76,7 +75,6 @@ namespace Jellyfin.Server.Extensions
                 {
                     // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
                     options.JsonSerializerOptions.PropertyNamingPolicy = null;
-                    options.JsonSerializerOptions.Converters.Add(new LongToStringConverter());
                 })
                 .AddControllersAsServices();
         }
diff --git a/Jellyfin.Server/Converters/LongToStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
similarity index 85%
rename from Jellyfin.Server/Converters/LongToStringConverter.cs
rename to MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
index ad66b7b0c3..d18fd95d5f 100644
--- a/Jellyfin.Server/Converters/LongToStringConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs
@@ -5,16 +5,16 @@ using System.Globalization;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
-namespace Jellyfin.Server.Converters
+namespace MediaBrowser.Common.Json.Converters
 {
     /// <summary>
     /// Long to String JSON converter.
     /// Javascript does not support 64-bit integers.
     /// </summary>
-    public class LongToStringConverter : JsonConverter<long>
+    public class JsonInt64Converter : JsonConverter<long>
     {
         /// <summary>
-        /// Read JSON string as Long.
+        /// Read JSON string as int64.
         /// </summary>
         /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
         /// <param name="type">Type.</param>
@@ -25,8 +25,8 @@ namespace Jellyfin.Server.Converters
             if (reader.TokenType == JsonTokenType.String)
             {
                 // try to parse number directly from bytes
-                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
-                if (Utf8Parser.TryParse(span, out long number, out int bytesConsumed) && span.Length == bytesConsumed)
+                var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+                if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed)
                 {
                     return number;
                 }
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 4a6ee0a793..a7f5fde050 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -23,6 +23,7 @@ namespace MediaBrowser.Common.Json
 
             options.Converters.Add(new JsonGuidConverter());
             options.Converters.Add(new JsonStringEnumConverter());
+            options.Converters.Add(new JsonInt64Converter());
 
             return options;
         }

From a4b3f2e32b68a61407876ab11343936b14cc1191 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 23 May 2020 18:19:49 -0600
Subject: [PATCH 124/463] Add missing route attribute

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 733f1e6d86..8e0f766978 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -183,6 +183,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
         /// <response code="200">Latest channel items returned.</response>
         /// <returns>Latest channel items.</returns>
+        [HttpGet("Items/Latest")]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,

From 70c42eb0acd0e6572f3ca9313716a8dd71b247ee Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 24 May 2020 12:19:26 -0600
Subject: [PATCH 125/463] Apply review suggestions

---
 .../Controllers/ChannelsController.cs         | 33 +++++++++++--------
 .../RequestHelpers.cs}                        |  0
 2 files changed, 19 insertions(+), 14 deletions(-)
 rename Jellyfin.Api/{Extensions/RequestExtensions.cs => Helpers/RequestHelpers.cs} (100%)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 8e0f766978..7c055874d7 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -42,14 +42,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available channels.
         /// </summary>
-        /// <param name="userId">User Id.</param>
+        /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
         /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
         /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
         /// <response code="200">Channels returned.</response>
-        /// <returns>Channels.</returns>
+        /// <returns>An <see cref="OkResult"/> containing the channels.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetChannels(
@@ -75,10 +75,10 @@ namespace Jellyfin.Api.Controllers
         /// Get all channel features.
         /// </summary>
         /// <response code="200">All channel features returned.</response>
-        /// <returns>Channel features.</returns>
+        /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
         [HttpGet("Features")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IEnumerable<ChannelFeatures> GetAllChannelFeatures()
+        public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
         {
             return _channelManager.GetAllChannelFeatures();
         }
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="id">Channel id.</param>
         /// <response code="200">Channel features returned.</response>
-        /// <returns>Channel features.</returns>
+        /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
         [HttpGet("{Id}/Features")]
         public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string id)
         {
@@ -99,11 +99,11 @@ namespace Jellyfin.Api.Controllers
         /// Get channel items.
         /// </summary>
         /// <param name="id">Channel Id.</param>
-        /// <param name="folderId">Folder Id.</param>
-        /// <param name="userId">User Id.</param>
+        /// <param name="folderId">Optional. Folder Id.</param>
+        /// <param name="userId">Optional. User Id.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
         /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
@@ -175,14 +175,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets latest channel items.
         /// </summary>
-        /// <param name="userId">User Id.</param>
+        /// <param name="userId">Optional. User Id.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
         /// <response code="200">Latest channel items returned.</response>
-        /// <returns>Latest channel items.</returns>
+        /// <returns>
+        /// A <see cref="Task"/> representing the request to get the latest channel items.
+        /// The task result contains an <see cref="OkResult"/> containing the latest channel items.
+        /// </returns>
         [HttpGet("Items/Latest")]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
             [FromQuery] Guid? userId,
@@ -192,7 +195,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string fields,
             [FromQuery] string channelIds)
         {
-            var user = userId == null
+            var user = userId == null || userId == Guid.Empty
                 ? null
                 : _userManager.GetUserById(userId.Value);
 
@@ -200,9 +203,11 @@ namespace Jellyfin.Api.Controllers
             {
                 Limit = limit,
                 StartIndex = startIndex,
-                ChannelIds =
-                    (channelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i))
-                    .Select(i => new Guid(i)).ToArray(),
+                ChannelIds = (channelIds ?? string.Empty)
+                    .Split(',')
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Select(i => new Guid(i))
+                    .ToArray(),
                 DtoOptions = new DtoOptions { Fields = RequestExtensions.GetItemFields(fields) }
             };
 
diff --git a/Jellyfin.Api/Extensions/RequestExtensions.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
similarity index 100%
rename from Jellyfin.Api/Extensions/RequestExtensions.cs
rename to Jellyfin.Api/Helpers/RequestHelpers.cs

From 1f9cda6a6600a81969cf8afad3c60fa77bb5a093 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 24 May 2020 12:19:37 -0600
Subject: [PATCH 126/463] Add ImageTags to SwaggerGenTypes

---
 .../ApiServiceCollectionExtensions.cs         | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 71ef9a69a2..4d00c513ba 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,13 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
 
 namespace Jellyfin.Server.Extensions
 {
@@ -89,7 +93,25 @@ namespace Jellyfin.Server.Extensions
             return serviceCollection.AddSwaggerGen(c =>
             {
                 c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
+                c.MapSwaggerGenTypes();
             });
         }
+
+        private static void MapSwaggerGenTypes(this SwaggerGenOptions options)
+        {
+            // BaseItemDto.ImageTags
+            options.MapType<Dictionary<ImageType, string>>(() =>
+                new OpenApiSchema
+                {
+                    Type = "object",
+                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+                        name => name,
+                        name => new OpenApiSchema
+                        {
+                            Type = "string",
+                            Format = "string"
+                        })
+                });
+        }
     }
 }

From 40762f13c6d4ebe0baef2cc241dfbb2a908999b4 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 24 May 2020 15:54:34 -0600
Subject: [PATCH 127/463] Fix route parameter casing

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index f35f822019..e25b4c8219 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -86,19 +86,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get channel features.
         /// </summary>
-        /// <param name="id">Channel id.</param>
+        /// <param name="channelId">Channel id.</param>
         /// <response code="200">Channel features returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
-        [HttpGet("{Id}/Features")]
-        public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string id)
+        [HttpGet("{channelId}/Features")]
+        public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string channelId)
         {
-            return _channelManager.GetChannelFeatures(id);
+            return _channelManager.GetChannelFeatures(channelId);
         }
 
         /// <summary>
         /// Get channel items.
         /// </summary>
-        /// <param name="id">Channel Id.</param>
+        /// <param name="channelId">Channel Id.</param>
         /// <param name="folderId">Optional. Folder Id.</param>
         /// <param name="userId">Optional. User Id.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
@@ -109,9 +109,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <response code="200">Channel items returned.</response>
         /// <returns>Channel items.</returns>
-        [HttpGet("{Id}/Items")]
+        [HttpGet("{channelId}/Items")]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
-            [FromRoute] Guid id,
+            [FromRoute] Guid channelId,
             [FromQuery] Guid? folderId,
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
             {
                 Limit = limit,
                 StartIndex = startIndex,
-                ChannelIds = new[] { id },
+                ChannelIds = new[] { channelId },
                 ParentId = folderId ?? Guid.Empty,
                 OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                 DtoOptions = new DtoOptions { Fields = RequestHelpers.GetItemFields(fields) }

From 483e24607b92b2989158670ba4f36da0361d52e2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 24 May 2020 16:01:53 -0600
Subject: [PATCH 128/463] Fix optional parameter binding

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index e25b4c8219..6b42b500e0 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -116,10 +116,10 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string sortOrder,
-            [FromQuery] string filters,
-            [FromQuery] string sortBy,
-            [FromQuery] string fields)
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? filters,
+            [FromQuery] string? sortBy,
+            [FromQuery] string? fields)
         {
             var user = userId == null
                 ? null

From e02cc8da53ff76a17de52a18ad83e73a1caa6394 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 24 May 2020 16:04:47 -0600
Subject: [PATCH 129/463] Add Swashbuckle TODO note

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 4d08cddbc0..7bb659ecee 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -120,6 +120,8 @@ namespace Jellyfin.Server.Extensions
                     description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
 
                 // Add types not supported by System.Text.Json
+                // TODO: Remove this once these types are supported by System.Text.Json and Swashbuckle
+                // See: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1667
                 c.MapSwaggerGenTypes();
             });
         }

From a5a39300bc733ad7b1d3c683f5f290a742171661 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 26 May 2020 16:55:27 +0200
Subject: [PATCH 130/463] Don't send Exception message in Production
 Environment

---
 .../Middleware/ExceptionMiddleware.cs            | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 0d79bbfaff..dd4d1ee99b 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -7,7 +7,9 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server.Middleware
@@ -20,6 +22,7 @@ namespace Jellyfin.Server.Middleware
         private readonly RequestDelegate _next;
         private readonly ILogger<ExceptionMiddleware> _logger;
         private readonly IServerConfigurationManager _configuration;
+        private readonly IWebHostEnvironment _hostEnvironment;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
@@ -27,14 +30,17 @@ namespace Jellyfin.Server.Middleware
         /// <param name="next">Next request delegate.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param>
         public ExceptionMiddleware(
             RequestDelegate next,
             ILogger<ExceptionMiddleware> logger,
-            IServerConfigurationManager serverConfigurationManager)
+            IServerConfigurationManager serverConfigurationManager,
+            IWebHostEnvironment hostEnvironment)
         {
             _next = next;
             _logger = logger;
             _configuration = serverConfigurationManager;
+            _hostEnvironment = hostEnvironment;
         }
 
         /// <summary>
@@ -85,6 +91,14 @@ namespace Jellyfin.Server.Middleware
 
                 context.Response.StatusCode = GetStatusCode(ex);
                 context.Response.ContentType = MediaTypeNames.Text.Plain;
+
+                // Don't send exception unless the server is in a Development environment
+                if (!_hostEnvironment.IsDevelopment())
+                {
+                    await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false);
+                    return;
+                }
+
                 var errorContent = NormalizeExceptionMessage(ex.Message);
                 await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
             }

From b944b8f8c54963f61eee5eeb97cd1745ae42ac50 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 11:03:08 -0600
Subject: [PATCH 131/463] Enable CORS and Authentication.

---
 .../ApiServiceCollectionExtensions.cs         |  8 ++++-
 Jellyfin.Server/Models/ServerCorsPolicy.cs    | 30 +++++++++++++++++++
 Jellyfin.Server/Startup.cs                    |  4 ++-
 3 files changed, 40 insertions(+), 2 deletions(-)
 create mode 100644 Jellyfin.Server/Models/ServerCorsPolicy.cs

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 344ef6a5ff..239c71503a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -11,6 +11,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Server.Formatters;
+using Jellyfin.Server.Models;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -71,7 +72,12 @@ namespace Jellyfin.Server.Extensions
         /// <returns>The MVC builder.</returns>
         public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl)
         {
-            return serviceCollection.AddMvc(opts =>
+            return serviceCollection
+                .AddCors(options =>
+                {
+                    options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
+                })
+                .AddMvc(opts =>
                 {
                     opts.UseGeneralRoutePrefix(baseUrl);
                     opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs
new file mode 100644
index 0000000000..ae010c042e
--- /dev/null
+++ b/Jellyfin.Server/Models/ServerCorsPolicy.cs
@@ -0,0 +1,30 @@
+using Microsoft.AspNetCore.Cors.Infrastructure;
+
+namespace Jellyfin.Server.Models
+{
+    /// <summary>
+    /// Server Cors Policy.
+    /// </summary>
+    public static class ServerCorsPolicy
+    {
+        /// <summary>
+        /// Default policy name.
+        /// </summary>
+        public const string DefaultPolicyName = "DefaultCorsPolicy";
+
+        /// <summary>
+        /// Default Policy. Allow Everything.
+        /// </summary>
+        public static readonly CorsPolicy DefaultPolicy = new CorsPolicy
+        {
+            // Allow any origin
+            Origins = { "*" },
+
+            // Allow any method
+            Methods = { "*" },
+
+            // Allow any header
+            Headers = { "*" }
+        };
+    }
+}
\ No newline at end of file
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 7c49afbfc6..bd2887e4a0 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,5 +1,6 @@
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Middleware;
+using Jellyfin.Server.Models;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
@@ -68,9 +69,10 @@ namespace Jellyfin.Server
             // TODO app.UseMiddleware<WebSocketMiddleware>();
             app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
 
-            // TODO use when old API is removed: app.UseAuthentication();
+            app.UseAuthentication();
             app.UseJellyfinApiSwagger(_serverConfigurationManager);
             app.UseRouting();
+            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
             app.UseAuthorization();
             app.UseEndpoints(endpoints =>
             {

From 9f0b5f347a9b7eeff1d0b079ceee42bc781aed7a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 11:06:15 -0600
Subject: [PATCH 132/463] Switch Config controller to System.Text.Json

---
 .../Controllers/ConfigurationController.cs       | 16 +++-------------
 1 file changed, 3 insertions(+), 13 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 992cb00874..2a1dce74d4 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,12 +1,12 @@
 #nullable enable
 
+using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.ConfigurationDtos;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers
     {
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IJsonSerializer _jsonSerializer;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
         /// </summary>
         /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="jsonSerializer">Instance of the <see cref="IJsonSerializer"/> interface.</param>
         public ConfigurationController(
             IServerConfigurationManager configurationManager,
-            IMediaEncoder mediaEncoder,
-            IJsonSerializer jsonSerializer)
+            IMediaEncoder mediaEncoder)
         {
             _configurationManager = configurationManager;
             _mediaEncoder = mediaEncoder;
-            _jsonSerializer = jsonSerializer;
         }
 
         /// <summary>
@@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
-            /*
-            // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed.
-            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType);
-            */
-
-            var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType)
-                .ConfigureAwait(false);
+            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
             _configurationManager.SaveConfiguration(key, configuration);
             return Ok();
         }

From dfe873fc293cf940a4f3d25aacdc8dfc53f150dc Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 11:12:33 -0600
Subject: [PATCH 133/463] Add Authentication to openapi generation.

---
 .../ApiServiceCollectionExtensions.cs         | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 344ef6a5ff..a6817421a8 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -99,6 +99,25 @@ namespace Jellyfin.Server.Extensions
             return serviceCollection.AddSwaggerGen(c =>
             {
                 c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" });
+                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
+                {
+                    Type = SecuritySchemeType.ApiKey,
+                    In = ParameterLocation.Header,
+                    Name = "X-Emby-Token",
+                    Description = "API key header parameter"
+                });
+
+                var securitySchemeRef = new OpenApiSecurityScheme
+                {
+                    Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = AuthenticationSchemes.CustomAuthentication },
+                };
+
+                // TODO: Apply this with an operation filter instead of globally
+                // https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements
+                c.AddSecurityRequirement(new OpenApiSecurityRequirement
+                {
+                    { securitySchemeRef, Array.Empty<string>() }
+                });
 
                 // Add all xml doc files to swagger generator.
                 var xmlFiles = Directory.GetFiles(

From e30a85025f3d0f8b936827613239da7c2c2387c2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 12:42:59 -0600
Subject: [PATCH 134/463] Remove log spam when using legacy api

---
 .../HttpServer/Security/AuthService.cs                |  6 ++++++
 Jellyfin.Api/Auth/CustomAuthenticationHandler.cs      | 11 +++++++++--
 MediaBrowser.Controller/Net/AuthenticatedAttribute.cs |  4 ++++
 3 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 58421aaf19..18bea59ad5 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -146,11 +146,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 return true;
             }
+
             if (authAttribtues.AllowLocalOnly && request.IsLocal)
             {
                 return true;
             }
 
+            if (authAttribtues.IgnoreLegacyAuth)
+            {
+                return true;
+            }
+
             return false;
         }
 
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 26f7d9d2dd..a0c9c3f5aa 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -37,13 +37,20 @@ namespace Jellyfin.Api.Auth
         /// <inheritdoc />
         protected override Task<AuthenticateResult> HandleAuthenticateAsync()
         {
-            var authenticatedAttribute = new AuthenticatedAttribute();
+            var authenticatedAttribute = new AuthenticatedAttribute
+            {
+                IgnoreLegacyAuth = true
+            };
+
             try
             {
                 var user = _authService.Authenticate(Request, authenticatedAttribute);
                 if (user == null)
                 {
-                    return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
+                    return Task.FromResult(AuthenticateResult.NoResult());
+                    // TODO return when legacy API is removed.
+                    // Don't spam the log with "Invalid User"
+                    // return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
                 }
 
                 var claims = new[]
diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
index 29fb81e32a..9f2743ea18 100644
--- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
+++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
@@ -52,6 +52,8 @@ namespace MediaBrowser.Controller.Net
             return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
         }
 
+        public bool IgnoreLegacyAuth { get; set; }
+        
         public bool AllowLocalOnly { get; set; }
     }
 
@@ -63,5 +65,7 @@ namespace MediaBrowser.Controller.Net
         bool AllowLocalOnly { get; }
 
         string[] GetRoles();
+        
+        bool IgnoreLegacyAuth { get; }
     }
 }

From aed6f57f11e4d08372fcf456742bdaedea374f6d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 20:54:02 -0600
Subject: [PATCH 135/463] Remove invalid docs and null check

---
 .../Controllers/DisplayPreferencesController.cs        | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 579b5df5d4..35efe6b5f8 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -35,7 +35,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User id.</param>
         /// <param name="client">Client.</param>
         /// <response code="200">Display preferences retrieved.</response>
-        /// <response code="404">Specified display preferences not found.</response>
         /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
         [HttpGet("{DisplayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -45,13 +44,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] [Required] string userId,
             [FromQuery] [Required] string client)
         {
-            var result = _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
-            if (result == null)
-            {
-                return NotFound();
-            }
-
-            return result;
+            return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
         }
 
         /// <summary>
@@ -62,7 +55,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="client">Client.</param>
         /// <param name="displayPreferences">New Display Preferences object.</param>
         /// <response code="200">Display preferences updated.</response>
-        /// <response code="404">Specified display preferences not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
         [HttpPost("{DisplayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 6d9f564a949e326e909cbcfd37d254195b40ba56 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 2 Jun 2020 15:04:55 +0200
Subject: [PATCH 136/463] Remove duplicate code

Co-authored-by: Vasily <JustAMan@users.noreply.github.com>
---
 Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index dd4d1ee99b..63effafc19 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -93,13 +93,9 @@ namespace Jellyfin.Server.Middleware
                 context.Response.ContentType = MediaTypeNames.Text.Plain;
 
                 // Don't send exception unless the server is in a Development environment
-                if (!_hostEnvironment.IsDevelopment())
-                {
-                    await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false);
-                    return;
-                }
-
-                var errorContent = NormalizeExceptionMessage(ex.Message);
+                var errorContent = _hostEnvironment.IsDevelopment()
+                        ? NormalizeExceptionMessage(ex.Message)
+                        : "Error processing request.";
                 await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
             }
         }

From 638cfa32abc3ffd61119f774ffc5a0f5a490d3e9 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 2 Jun 2020 15:07:07 +0200
Subject: [PATCH 137/463] Move SearchService to new API endpoint

---
 Jellyfin.Api/BaseJellyfinApiController.cs    |  19 ++
 Jellyfin.Api/Controllers/SearchController.cs | 266 +++++++++++++++
 MediaBrowser.Api/SearchService.cs            | 333 -------------------
 3 files changed, 285 insertions(+), 333 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SearchController.cs
 delete mode 100644 MediaBrowser.Api/SearchService.cs

diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 1f4508e6cb..6a9e48f8dd 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,3 +1,4 @@
+using System;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
@@ -9,5 +10,23 @@ namespace Jellyfin.Api
     [Route("[controller]")]
     public class BaseJellyfinApiController : ControllerBase
     {
+        /// <summary>
+        /// Splits a string at a seperating character into an array of substrings.
+        /// </summary>
+        /// <param name="value">The string to split.</param>
+        /// <param name="separator">The char that seperates the substrings.</param>
+        /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
+        /// <returns>An array of the substrings.</returns>
+        internal static string[] Split(string value, char separator, bool removeEmpty)
+        {
+            if (string.IsNullOrWhiteSpace(value))
+            {
+                return Array.Empty<string>();
+            }
+
+            return removeEmpty
+                ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+                : value.Split(separator);
+        }
     }
 }
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
new file mode 100644
index 0000000000..15a650bf97
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -0,0 +1,266 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Search;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Search controller.
+    /// </summary>
+    [Route("/Search/Hints")]
+    [Authenticated]
+    public class SearchController : BaseJellyfinApiController
+    {
+        private readonly ISearchEngine _searchEngine;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IImageProcessor _imageProcessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SearchController"/> class.
+        /// </summary>
+        /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+        /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+        public SearchController(
+            ISearchEngine searchEngine,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            IImageProcessor imageProcessor)
+        {
+            _searchEngine = searchEngine;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _imageProcessor = imageProcessor;
+        }
+
+        /// <summary>
+        /// Gets the search hint result.
+        /// </summary>
+        /// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">The maximum number of records to return.</param>
+        /// <param name="userId">Supply a user id to search within a user's library or omit to search all.</param>
+        /// <param name="searchTerm">The search term to filter on.</param>
+        /// <param name="includePeople">Optional filter whether to include people.</param>
+        /// <param name="includeMedia">Optional filter whether to include media.</param>
+        /// <param name="includeGenres">Optional filter whether to include genres.</param>
+        /// <param name="includeStudios">Optional filter whether to include studios.</param>
+        /// <param name="includeArtists">Optional filter whether to include artists.</param>
+        /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
+        /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
+        /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
+        /// <param name="parentId">If specified, only children of the parent are returned.</param>
+        /// <param name="isMovie">Optional filter for movies.</param>
+        /// <param name="isSeries">Optional filter for series.</param>
+        /// <param name="isNews">Optional filter for news.</param>
+        /// <param name="isKids">Optional filter for kids.</param>
+        /// <param name="isSports">Optional filter for sports.</param>
+        /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
+        [HttpGet]
+        [Description("Gets search hints based on a search term")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<SearchHintResult> Get(
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] Guid userId,
+            [FromQuery, Required] string searchTerm,
+            [FromQuery] bool includePeople,
+            [FromQuery] bool includeMedia,
+            [FromQuery] bool includeGenres,
+            [FromQuery] bool includeStudios,
+            [FromQuery] bool includeArtists,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string parentId,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isSports)
+        {
+            var result = _searchEngine.GetSearchHints(new SearchQuery
+            {
+                Limit = limit,
+                SearchTerm = searchTerm,
+                IncludeArtists = includeArtists,
+                IncludeGenres = includeGenres,
+                IncludeMedia = includeMedia,
+                IncludePeople = includePeople,
+                IncludeStudios = includeStudios,
+                StartIndex = startIndex,
+                UserId = userId,
+                IncludeItemTypes = Split(includeItemTypes, ',', true),
+                ExcludeItemTypes = Split(excludeItemTypes, ',', true),
+                MediaTypes = Split(mediaTypes, ',', true),
+                ParentId = parentId,
+
+                IsKids = isKids,
+                IsMovie = isMovie,
+                IsNews = isNews,
+                IsSeries = isSeries,
+                IsSports = isSports
+            });
+
+            return Ok(new SearchHintResult
+            {
+                TotalRecordCount = result.TotalRecordCount,
+                SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
+            });
+        }
+
+        /// <summary>
+        /// Gets the search hint result.
+        /// </summary>
+        /// <param name="hintInfo">The hint info.</param>
+        /// <returns>SearchHintResult.</returns>
+        private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+        {
+            var item = hintInfo.Item;
+
+            var result = new SearchHint
+            {
+                Name = item.Name,
+                IndexNumber = item.IndexNumber,
+                ParentIndexNumber = item.ParentIndexNumber,
+                Id = item.Id,
+                Type = item.GetClientTypeName(),
+                MediaType = item.MediaType,
+                MatchedTerm = hintInfo.MatchedTerm,
+                RunTimeTicks = item.RunTimeTicks,
+                ProductionYear = item.ProductionYear,
+                ChannelId = item.ChannelId,
+                EndDate = item.EndDate
+            };
+
+            // legacy
+            result.ItemId = result.Id;
+
+            if (item.IsFolder)
+            {
+                result.IsFolder = true;
+            }
+
+            var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
+
+            if (primaryImageTag != null)
+            {
+                result.PrimaryImageTag = primaryImageTag;
+                result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
+            }
+
+            SetThumbImageInfo(result, item);
+            SetBackdropImageInfo(result, item);
+
+            switch (item)
+            {
+                case IHasSeries hasSeries:
+                    result.Series = hasSeries.SeriesName;
+                    break;
+                case LiveTvProgram program:
+                    result.StartDate = program.StartDate;
+                    break;
+                case Series series:
+                    if (series.Status.HasValue)
+                    {
+                        result.Status = series.Status.Value.ToString();
+                    }
+
+                    break;
+                case MusicAlbum album:
+                    result.Artists = album.Artists;
+                    result.AlbumArtist = album.AlbumArtist;
+                    break;
+                case Audio song:
+                    result.AlbumArtist = song.AlbumArtists?[0];
+                    result.Artists = song.Artists;
+
+                    MusicAlbum musicAlbum = song.AlbumEntity;
+
+                    if (musicAlbum != null)
+                    {
+                        result.Album = musicAlbum.Name;
+                        result.AlbumId = musicAlbum.Id;
+                    }
+                    else
+                    {
+                        result.Album = song.Album;
+                    }
+
+                    break;
+            }
+
+            if (!item.ChannelId.Equals(Guid.Empty))
+            {
+                var channel = _libraryManager.GetItemById(item.ChannelId);
+                result.ChannelName = channel?.Name;
+            }
+
+            return result;
+        }
+
+        private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+        {
+            var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+
+            if (itemWithImage == null && item is Episode)
+            {
+                itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
+            }
+
+            if (itemWithImage == null)
+            {
+                itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
+            }
+
+            if (itemWithImage != null)
+            {
+                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
+
+                if (tag != null)
+                {
+                    hint.ThumbImageTag = tag;
+                    hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+                }
+            }
+        }
+
+        private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+        {
+            var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
+                ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
+
+            if (itemWithImage != null)
+            {
+                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
+
+                if (tag != null)
+                {
+                    hint.BackdropImageTag = tag;
+                    hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
+                }
+            }
+        }
+
+        private T GetParentWithImage<T>(BaseItem item, ImageType type)
+            where T : BaseItem
+        {
+            return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
+        }
+    }
+}
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs
deleted file mode 100644
index e9d339c6e3..0000000000
--- a/MediaBrowser.Api/SearchService.cs
+++ /dev/null
@@ -1,333 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Search;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetSearchHints
-    /// </summary>
-    [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")]
-    public class GetSearchHints : IReturn<SearchHintResult>
-    {
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Supply a user id to search within a user's library or omit to search all.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Search characters used to find items
-        /// </summary>
-        /// <value>The index by.</value>
-        [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SearchTerm { get; set; }
-
-
-        [ApiMember(Name = "IncludePeople", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludePeople { get; set; }
-
-        [ApiMember(Name = "IncludeMedia", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeMedia { get; set; }
-
-        [ApiMember(Name = "IncludeGenres", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeGenres { get; set; }
-
-        [ApiMember(Name = "IncludeStudios", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeStudios { get; set; }
-
-        [ApiMember(Name = "IncludeArtists", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeArtists { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        public GetSearchHints()
-        {
-            IncludeArtists = true;
-            IncludeGenres = true;
-            IncludeMedia = true;
-            IncludePeople = true;
-            IncludeStudios = true;
-        }
-    }
-
-    /// <summary>
-    /// Class SearchService
-    /// </summary>
-    [Authenticated]
-    public class SearchService : BaseApiService
-    {
-        /// <summary>
-        /// The _search engine
-        /// </summary>
-        private readonly ISearchEngine _searchEngine;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IImageProcessor _imageProcessor;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SearchService" /> class.
-        /// </summary>
-        /// <param name="searchEngine">The search engine.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="dtoService">The dto service.</param>
-        /// <param name="imageProcessor">The image processor.</param>
-        public SearchService(
-            ILogger<SearchService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISearchEngine searchEngine,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IImageProcessor imageProcessor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _searchEngine = searchEngine;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _imageProcessor = imageProcessor;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSearchHints request)
-        {
-            var result = GetSearchHintsAsync(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the search hints async.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{IEnumerable{SearchHintResult}}.</returns>
-        private SearchHintResult GetSearchHintsAsync(GetSearchHints request)
-        {
-            var result = _searchEngine.GetSearchHints(new SearchQuery
-            {
-                Limit = request.Limit,
-                SearchTerm = request.SearchTerm,
-                IncludeArtists = request.IncludeArtists,
-                IncludeGenres = request.IncludeGenres,
-                IncludeMedia = request.IncludeMedia,
-                IncludePeople = request.IncludePeople,
-                IncludeStudios = request.IncludeStudios,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
-                ExcludeItemTypes = ApiEntryPoint.Split(request.ExcludeItemTypes, ',', true),
-                MediaTypes = ApiEntryPoint.Split(request.MediaTypes, ',', true),
-                ParentId = request.ParentId,
-
-                IsKids = request.IsKids,
-                IsMovie = request.IsMovie,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries,
-                IsSports = request.IsSports
-
-            });
-
-            return new SearchHintResult
-            {
-                TotalRecordCount = result.TotalRecordCount,
-
-                SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
-            };
-        }
-
-        /// <summary>
-        /// Gets the search hint result.
-        /// </summary>
-        /// <param name="hintInfo">The hint info.</param>
-        /// <returns>SearchHintResult.</returns>
-        private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
-        {
-            var item = hintInfo.Item;
-
-            var result = new SearchHint
-            {
-                Name = item.Name,
-                IndexNumber = item.IndexNumber,
-                ParentIndexNumber = item.ParentIndexNumber,
-                Id = item.Id,
-                Type = item.GetClientTypeName(),
-                MediaType = item.MediaType,
-                MatchedTerm = hintInfo.MatchedTerm,
-                RunTimeTicks = item.RunTimeTicks,
-                ProductionYear = item.ProductionYear,
-                ChannelId = item.ChannelId,
-                EndDate = item.EndDate
-            };
-
-            // legacy
-            result.ItemId = result.Id;
-
-            if (item.IsFolder)
-            {
-                result.IsFolder = true;
-            }
-
-            var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
-
-            if (primaryImageTag != null)
-            {
-                result.PrimaryImageTag = primaryImageTag;
-                result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
-            }
-
-            SetThumbImageInfo(result, item);
-            SetBackdropImageInfo(result, item);
-
-            switch (item)
-            {
-                case IHasSeries hasSeries:
-                    result.Series = hasSeries.SeriesName;
-                    break;
-                case LiveTvProgram program:
-                    result.StartDate = program.StartDate;
-                    break;
-                case Series series:
-                    if (series.Status.HasValue)
-                    {
-                        result.Status = series.Status.Value.ToString();
-                    }
-
-                    break;
-                case MusicAlbum album:
-                    result.Artists = album.Artists;
-                    result.AlbumArtist = album.AlbumArtist;
-                    break;
-                case Audio song:
-                    result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
-                    result.Artists = song.Artists;
-
-                    MusicAlbum musicAlbum = song.AlbumEntity;
-
-                    if (musicAlbum != null)
-                    {
-                        result.Album = musicAlbum.Name;
-                        result.AlbumId = musicAlbum.Id;
-                    }
-                    else
-                    {
-                        result.Album = song.Album;
-                    }
-
-                    break;
-            }
-
-            if (!item.ChannelId.Equals(Guid.Empty))
-            {
-                var channel = _libraryManager.GetItemById(item.ChannelId);
-                result.ChannelName = channel?.Name;
-            }
-
-            return result;
-        }
-
-        private void SetThumbImageInfo(SearchHint hint, BaseItem item)
-        {
-            var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
-
-            if (itemWithImage == null && item is Episode)
-            {
-                itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
-            }
-
-            if (itemWithImage == null)
-            {
-                itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
-            }
-
-            if (itemWithImage != null)
-            {
-                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
-
-                if (tag != null)
-                {
-                    hint.ThumbImageTag = tag;
-                    hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
-                }
-            }
-        }
-
-        private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
-        {
-            var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
-                ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
-
-            if (itemWithImage != null)
-            {
-                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
-
-                if (tag != null)
-                {
-                    hint.BackdropImageTag = tag;
-                    hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
-                }
-            }
-        }
-
-        private T GetParentWithImage<T>(BaseItem item, ImageType type)
-            where T : BaseItem
-        {
-            return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
-        }
-    }
-}

From 4fe0beec162d4554f1d6cc3c658b672eafbfa307 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 21 May 2020 08:44:15 -0600
Subject: [PATCH 138/463] Fix Json Enum conversion, map all JsonDefaults
 properties to API

---
 .../ApiServiceCollectionExtensions.cs         | 22 +++++++---
 .../CamelCaseJsonProfileFormatter.cs          |  4 +-
 .../PascalCaseJsonProfileFormatter.cs         |  4 +-
 Jellyfin.Server/Models/JsonOptions.cs         | 41 -------------------
 MediaBrowser.Common/Json/JsonDefaults.cs      | 34 ++++++++++++++-
 5 files changed, 53 insertions(+), 52 deletions(-)
 delete mode 100644 Jellyfin.Server/Models/JsonOptions.cs

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 344ef6a5ff..b9f55e2008 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,9 +1,6 @@
 using System;
-using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using System.Reflection;
-using System.Text.Json.Serialization;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
@@ -11,6 +8,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Server.Formatters;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -83,8 +81,20 @@ namespace Jellyfin.Server.Extensions
                 .AddApplicationPart(typeof(StartupController).Assembly)
                 .AddJsonOptions(options =>
                 {
-                    // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
-                    options.JsonSerializerOptions.PropertyNamingPolicy = null;
+                    // Update all properties that are set in JsonDefaults
+                    var jsonOptions = JsonDefaults.PascalCase;
+
+                    // From JsonDefaults
+                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
+                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
+                    options.JsonSerializerOptions.Converters.Clear();
+                    foreach (var converter in jsonOptions.Converters)
+                    {
+                        options.JsonSerializerOptions.Converters.Add(converter);
+                    }
+
+                    // From JsonDefaults.PascalCase
+                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
                 })
                 .AddControllersAsServices();
         }
@@ -98,7 +108,7 @@ namespace Jellyfin.Server.Extensions
         {
             return serviceCollection.AddSwaggerGen(c =>
             {
-                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" });
+                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
 
                 // Add all xml doc files to swagger generator.
                 var xmlFiles = Directory.GetFiles(
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
index e6ad6dfb13..989c8ecea2 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Server.Models;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
 
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase)
+        public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase)
         {
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
index 675f4c79ee..69963b3fb3 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Server.Models;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
 
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase)
+        public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase)
         {
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs
deleted file mode 100644
index 2f0df3d2c7..0000000000
--- a/Jellyfin.Server/Models/JsonOptions.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Text.Json;
-
-namespace Jellyfin.Server.Models
-{
-    /// <summary>
-    /// Json Options.
-    /// </summary>
-    public static class JsonOptions
-    {
-        /// <summary>
-        /// Gets CamelCase json options.
-        /// </summary>
-        public static JsonSerializerOptions CamelCase
-        {
-            get
-            {
-                var options = DefaultJsonOptions;
-                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-                return options;
-            }
-        }
-
-        /// <summary>
-        /// Gets PascalCase json options.
-        /// </summary>
-        public static JsonSerializerOptions PascalCase
-        {
-            get
-            {
-                var options = DefaultJsonOptions;
-                options.PropertyNamingPolicy = null;
-                return options;
-            }
-        }
-
-        /// <summary>
-        /// Gets base Json Serializer Options.
-        /// </summary>
-        private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions();
-    }
-}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 4a6ee0a793..326f04eea1 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json
         /// <summary>
         /// Gets the default <see cref="JsonSerializerOptions" /> options.
         /// </summary>
+        /// <remarks>
+        /// When changing these options, update
+        ///     Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+        ///         -> AddJellyfinApi
+        ///             -> AddJsonOptions
+        /// </remarks>
         /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
         public static JsonSerializerOptions GetOptions()
         {
-            var options = new JsonSerializerOptions()
+            var options = new JsonSerializerOptions
             {
                 ReadCommentHandling = JsonCommentHandling.Disallow,
                 WriteIndented = false
@@ -26,5 +32,31 @@ namespace MediaBrowser.Common.Json
 
             return options;
         }
+        
+        /// <summary>
+        /// Gets CamelCase json options.
+        /// </summary>
+        public static JsonSerializerOptions CamelCase
+        {
+            get
+            {
+                var options = GetOptions();
+                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+                return options;
+            }
+        }
+
+        /// <summary>
+        /// Gets PascalCase json options.
+        /// </summary>
+        public static JsonSerializerOptions PascalCase
+        {
+            get
+            {
+                var options = GetOptions();
+                options.PropertyNamingPolicy = null;
+                return options;
+            }
+        }
     }
 }

From 0e41c4727d84edbf4d7b96c59e0a3d3bec87b633 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 1 Jun 2020 10:36:32 -0600
Subject: [PATCH 139/463] revert to System.Text.JsonSerializer

---
 .../Controllers/ConfigurationController.cs         | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 992cb00874..8243bfce4c 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,12 +1,12 @@
 #nullable enable
 
+using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.ConfigurationDtos;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers
     {
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IJsonSerializer _jsonSerializer;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
         /// </summary>
         /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="jsonSerializer">Instance of the <see cref="IJsonSerializer"/> interface.</param>
         public ConfigurationController(
             IServerConfigurationManager configurationManager,
-            IMediaEncoder mediaEncoder,
-            IJsonSerializer jsonSerializer)
+            IMediaEncoder mediaEncoder)
         {
             _configurationManager = configurationManager;
             _mediaEncoder = mediaEncoder;
-            _jsonSerializer = jsonSerializer;
         }
 
         /// <summary>
@@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
-            /*
-            // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed.
             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType);
-            */
-
-            var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType)
-                .ConfigureAwait(false);
             _configurationManager.SaveConfiguration(key, configuration);
             return Ok();
         }

From cf9cbfff5667961eebec04f20c844f9df988c5a7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 2 Jun 2020 08:28:37 -0600
Subject: [PATCH 140/463] Add second endpoint for Startup/User

---
 Jellyfin.Api/Controllers/StartupController.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index ed1dc1ede3..57a02e62a9 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -109,6 +109,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Initial user retrieved.</response>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
+        [HttpGet("FirstUser")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<StartupUserDto> GetFirstUser()
         {

From 2476848dd3d69252c5bc8b45a4eddf545354a0c0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 2 Jun 2020 10:12:55 -0600
Subject: [PATCH 141/463] Fix tests

---
 .../Auth/CustomAuthenticationHandlerTests.cs                  | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 3b3d03c8b7..437dfa410b 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -88,7 +88,9 @@ namespace Jellyfin.Api.Tests.Auth
             var authenticateResult = await _sut.AuthenticateAsync();
 
             Assert.False(authenticateResult.Succeeded);
-            Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+            Assert.True(authenticateResult.None);
+            // TODO return when legacy API is removed.
+            // Assert.Equal("Invalid user", authenticateResult.Failure.Message);
         }
 
         [Fact]

From 01a5103fef83bbbef230faf2303d16648981a5d2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 2 Jun 2020 11:47:00 -0600
Subject: [PATCH 142/463] Add Dictionary with non-string keys to
 System.Text.Json

---
 .../ApiServiceCollectionExtensions.cs         | 26 ++++++
 .../JsonNonStringKeyDictionaryConverter.cs    | 79 +++++++++++++++++++
 ...nNonStringKeyDictionaryConverterFactory.cs | 60 ++++++++++++++
 MediaBrowser.Common/Json/JsonDefaults.cs      |  1 +
 4 files changed, 166 insertions(+)
 create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
 create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index b9f55e2008..cb4189587d 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
@@ -9,6 +11,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Server.Formatters;
 using MediaBrowser.Common.Json;
+using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.Extensions.DependencyInjection;
@@ -128,7 +131,30 @@ namespace Jellyfin.Server.Extensions
                 // Use method name as operationId
                 c.CustomOperationIds(description =>
                     description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
+
+                // TODO - remove when all types are supported in System.Text.Json
+                c.AddSwaggerTypeMappings();
             });
         }
+
+        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
+        {
+            /*
+             * TODO remove when System.Text.Json supports non-string keys.
+             * Used in Jellyfin.Api.Controller.GetChannels.
+             */
+            options.MapType<Dictionary<ImageType, string>>(() =>
+                new OpenApiSchema
+                {
+                    Type = "object",
+                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+                        name => name,
+                        name => new OpenApiSchema
+                        {
+                            Type = "string",
+                            Format = "string"
+                        })
+                });
+        }
     }
 }
diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
new file mode 100644
index 0000000000..f2ddd7fea2
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
@@ -0,0 +1,79 @@
+#nullable enable
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// Converter for Dictionaries without string key.
+    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
+    /// </summary>
+    /// <typeparam name="TKey">Type of key.</typeparam>
+    /// <typeparam name="TValue">Type of value.</typeparam>
+    internal sealed class JsonNonStringKeyDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
+    {
+        /// <summary>
+        /// Read JSON.
+        /// </summary>
+        /// <param name="reader">The Utf8JsonReader.</param>
+        /// <param name="typeToConvert">The type to convert.</param>
+        /// <param name="options">The json serializer options.</param>
+        /// <returns>Typed dictionary.</returns>
+        /// <exception cref="NotSupportedException"></exception>
+        public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]);
+            var value = JsonSerializer.Deserialize(ref reader, convertedType, options);
+            var instance = (Dictionary<TKey, TValue>)Activator.CreateInstance(
+                typeToConvert,
+                BindingFlags.Instance | BindingFlags.Public,
+                null,
+                null,
+                CultureInfo.CurrentCulture);
+            var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null);
+            var parse = typeof(TKey).GetMethod(
+                "Parse", 
+                0, 
+                BindingFlags.Public | BindingFlags.Static, 
+                null, 
+                CallingConventions.Any, 
+                new[] { typeof(string) }, 
+                null);
+            if (parse == null)
+            {
+                throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary<TKey, TValue> is not supported.");
+            }
+            
+            while (enumerator.MoveNext())
+            {
+                var element = (KeyValuePair<string?, TValue>)enumerator.Current;
+                instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value);
+            }
+            
+            return instance;
+        }
+
+        /// <summary>
+        /// Write dictionary as Json.
+        /// </summary>
+        /// <param name="writer">The Utf8JsonWriter.</param>
+        /// <param name="value">The dictionary value.</param>
+        /// <param name="options">The Json serializer options.</param>
+        public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
+        {
+            var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
+            foreach (var (k, v) in value)
+            {
+                convertedDictionary[k?.ToString()] = v;
+            }
+            JsonSerializer.Serialize(writer, convertedDictionary, options);
+            convertedDictionary.Clear();
+        }
+    }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
new file mode 100644
index 0000000000..d9795a189a
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
@@ -0,0 +1,60 @@
+#nullable enable
+
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972.
+    /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
+    /// </summary>
+    internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory
+    {
+        /// <summary>
+        /// Only convert objects that implement IDictionary and do not have string keys.
+        /// </summary>
+        /// <param name="typeToConvert">Type convert.</param>
+        /// <returns>Conversion ability.</returns>
+        public override bool CanConvert(Type typeToConvert)
+        {
+            
+            if (!typeToConvert.IsGenericType)
+            {
+                return false;
+            }
+            
+            // Let built in converter handle string keys
+            if (typeToConvert.GenericTypeArguments[0] == typeof(string))
+            {
+                return false;
+            }
+            
+            // Only support objects that implement IDictionary
+            return typeToConvert.GetInterface(nameof(IDictionary)) != null;
+        }
+
+        /// <summary>
+        /// Create converter for generic dictionary type.
+        /// </summary>
+        /// <param name="typeToConvert">Type to convert.</param>
+        /// <param name="options">Json serializer options.</param>
+        /// <returns>JsonConverter for given type.</returns>
+        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>)
+                .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]);
+            var converter = (JsonConverter)Activator.CreateInstance(
+                converterType,
+                BindingFlags.Instance | BindingFlags.Public,
+                null,
+                null,
+                CultureInfo.CurrentCulture);
+            return converter;
+        }
+    }
+}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 326f04eea1..f38e2893ec 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -29,6 +29,7 @@ namespace MediaBrowser.Common.Json
 
             options.Converters.Add(new JsonGuidConverter());
             options.Converters.Add(new JsonStringEnumConverter());
+            options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
 
             return options;
         }

From 8b59934ccb53fda0ccfda2e914192ac3d1d11ad7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 3 Jun 2020 09:12:12 -0600
Subject: [PATCH 143/463] remove extra Clear call

---
 .../Json/Converters/JsonNonStringKeyDictionaryConverter.cs       | 1 -
 1 file changed, 1 deletion(-)

diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
index f2ddd7fea2..636ef5372f 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
@@ -73,7 +73,6 @@ namespace MediaBrowser.Common.Json.Converters
                 convertedDictionary[k?.ToString()] = v;
             }
             JsonSerializer.Serialize(writer, convertedDictionary, options);
-            convertedDictionary.Clear();
         }
     }
 }

From 3e749eabdf10f9a070a6c303ec37a912f9657e58 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 4 Jun 2020 07:29:00 -0600
Subject: [PATCH 144/463] Fix doc errors

---
 Jellyfin.Api/Controllers/DevicesController.cs | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index a46d3f9370..1e75579033 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,10 +1,8 @@
 #nullable enable
 
 using System;
-using System.Collections.Generic;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Devices;
@@ -45,8 +43,8 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get Devices.
         /// </summary>
-        /// <param name="supportsSync">/// Gets or sets a value indicating whether [supports synchronize].</param>
-        /// <param name="userId">/// Gets or sets the user identifier.</param>
+        /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
+        /// <param name="userId">Gets or sets the user identifier.</param>
         /// <response code="200">Devices retrieved.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
         [HttpGet]

From 22f56842bd6422a8f2789a2ce5a7d6f4caf563f2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 4 Jun 2020 07:59:11 -0600
Subject: [PATCH 145/463] Apply review suggestions

---
 .../Controllers/EnvironmentController.cs      | 31 +++++++++----------
 1 file changed, 14 insertions(+), 17 deletions(-)

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 8d9d2642f4..35cd89e0e8 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -21,17 +22,20 @@ namespace Jellyfin.Api.Controllers
     public class EnvironmentController : BaseJellyfinApiController
     {
         private const char UncSeparator = '\\';
-        private const string UncSeparatorString = "\\";
+        private const string UncStartPrefix = @"\\";
 
         private readonly IFileSystem _fileSystem;
+        private readonly ILogger<EnvironmentController> _logger;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
         /// </summary>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        public EnvironmentController(IFileSystem fileSystem)
+        /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
+        public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
         {
             _fileSystem = fileSystem;
+            _logger = logger;
         }
 
         /// <summary>
@@ -46,27 +50,19 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
             [FromQuery, BindRequired] string path,
-            [FromQuery] bool includeFiles,
-            [FromQuery] bool includeDirectories)
+            [FromQuery] bool includeFiles = false,
+            [FromQuery] bool includeDirectories = false)
         {
-            const string networkPrefix = UncSeparatorString + UncSeparatorString;
-            if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase)
+            if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
                 && path.LastIndexOf(UncSeparator) == 1)
             {
                 return Array.Empty<FileSystemEntryInfo>();
             }
 
-            var entries = _fileSystem.GetFileSystemEntries(path).OrderBy(i => i.FullName).Where(i =>
-            {
-                var isDirectory = i.IsDirectory;
-
-                if (!includeFiles && !isDirectory)
-                {
-                    return false;
-                }
-
-                return includeDirectories || !isDirectory;
-            });
+            var entries =
+                _fileSystem.GetFileSystemEntries(path)
+                    .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
+                    .OrderBy(i => i.FullName);
 
             return entries.Select(f => new FileSystemEntryInfo
             {
@@ -142,6 +138,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
         {
+            _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
             return Array.Empty<FileSystemEntryInfo>();
         }
 

From fd913d73e35491c64e39a76a266689c302a91b11 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 4 Jun 2020 10:10:36 -0600
Subject: [PATCH 146/463] Revert authorized endpoints to legacy api

---
 .../Controllers/Images/ImageByNameController.cs   | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
index fa60809773..db475d6b47 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
@@ -21,7 +21,6 @@ namespace Jellyfin.Api.Controllers.Images
     ///     Images By Name Controller.
     /// </summary>
     [Route("Images")]
-    [Authorize]
     public class ImageByNameController : BaseJellyfinApiController
     {
         private readonly IServerApplicationPaths _applicationPaths;
@@ -46,6 +45,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="200">Retrieved list of images.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("General")]
+        [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
         {
@@ -61,6 +61,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("General/{Name}/{Type}")]
+        [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -70,11 +71,11 @@ namespace Jellyfin.Api.Controllers.Images
                 ? "folder"
                 : type;
 
-            var paths = BaseItem.SupportedImageExtensions
-                .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)).ToList();
+            var path = BaseItem.SupportedImageExtensions
+                .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
+                .FirstOrDefault(System.IO.File.Exists);
 
-            var path = paths.FirstOrDefault(System.IO.File.Exists) ?? paths.FirstOrDefault();
-            if (path == null || !System.IO.File.Exists(path))
+            if (path == null)
             {
                 return NotFound();
             }
@@ -89,6 +90,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="200">Retrieved list of images.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("Ratings")]
+        [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
         {
@@ -104,6 +106,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("Ratings/{Theme}/{Name}")]
+        [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -120,6 +123,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="200">Image list retrieved.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("MediaInfo")]
+        [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
         {
@@ -135,6 +139,7 @@ namespace Jellyfin.Api.Controllers.Images
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         [HttpGet("MediaInfo/{Theme}/{Name}")]
+        [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]

From 6c53e36ccf1f27defae6faa5791598258bc604ab Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 4 Jun 2020 15:17:05 -0600
Subject: [PATCH 147/463] Fix Api Routing

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 8 ++++----
 MediaBrowser.Common/Json/JsonDefaults.cs             | 1 +
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 19cce974ea..e37e137d17 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -70,7 +70,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Task retrieved.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
-        [HttpGet("{TaskID}")]
+        [HttpGet("{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<TaskInfo> GetTask([FromRoute] string taskId)
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Task started.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
-        [HttpPost("Running/{TaskID}")]
+        [HttpPost("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult StartTask([FromRoute] string taskId)
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Task stopped.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
-        [HttpDelete("Running/{TaskID}")]
+        [HttpDelete("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult StopTask([FromRoute] string taskId)
@@ -142,7 +142,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Task triggers updated.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
-        [HttpPost("{TaskID}/Triggers")]
+        [HttpPost("{taskId}/Triggers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateTask(
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index f38e2893ec..35925c3a26 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -30,6 +30,7 @@ namespace MediaBrowser.Common.Json
             options.Converters.Add(new JsonGuidConverter());
             options.Converters.Add(new JsonStringEnumConverter());
             options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
+            options.Converters.Add(new JsonInt64Converter());
 
             return options;
         }

From 598cd94c4d5fd8911c9e30af7be47ca6eac7c975 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 6 Jun 2020 16:51:21 -0600
Subject: [PATCH 148/463] Add CSS output formatter.

---
 .../ApiServiceCollectionExtensions.cs         |  2 +
 .../Formatters/CssOutputFormatter.cs          | 37 +++++++++++++++++++
 2 files changed, 39 insertions(+)
 create mode 100644 Jellyfin.Server/Formatters/CssOutputFormatter.cs

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index cb4189587d..1c7fcbc066 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -77,6 +77,8 @@ namespace Jellyfin.Server.Extensions
                     opts.UseGeneralRoutePrefix(baseUrl);
                     opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
                     opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
+
+                    opts.OutputFormatters.Add(new CssOutputFormatter());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
new file mode 100644
index 0000000000..b26e7f96a0
--- /dev/null
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Formatters;
+
+namespace Jellyfin.Server.Formatters
+{
+    /// <summary>
+    /// Css output formatter.
+    /// </summary>
+    public class CssOutputFormatter : TextOutputFormatter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
+        /// </summary>
+        public CssOutputFormatter()
+        {
+            SupportedMediaTypes.Clear();
+            SupportedMediaTypes.Add("text/css");
+
+            SupportedEncodings.Add(Encoding.UTF8);
+            SupportedEncodings.Add(Encoding.Unicode);
+        }
+
+        /// <summary>
+        /// Write context object to stream.
+        /// </summary>
+        /// <param name="context">Writer context.</param>
+        /// <param name="selectedEncoding">Unused. Writer encoding.</param>
+        /// <returns>Write stream task.</returns>
+        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
+        {
+            return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+        }
+    }
+}
\ No newline at end of file

From 04abb281c0905f1dbd21365d98756790dbb30973 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 6 Jun 2020 16:52:23 -0600
Subject: [PATCH 149/463] Add CSS output formatter.

---
 Jellyfin.Server/Formatters/CssOutputFormatter.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
index b26e7f96a0..1dbddc79a8 100644
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -34,4 +34,4 @@ namespace Jellyfin.Server.Formatters
             return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
         }
     }
-}
\ No newline at end of file
+}

From 48cbac934ba593fc5f4fed0eb0db81061eb4a787 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 6 Jun 2020 16:53:49 -0600
Subject: [PATCH 150/463] Don't clear media types

---
 Jellyfin.Server/Formatters/CssOutputFormatter.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
index 1dbddc79a8..b3771b7fe6 100644
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -16,7 +16,6 @@ namespace Jellyfin.Server.Formatters
         /// </summary>
         public CssOutputFormatter()
         {
-            SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add("text/css");
 
             SupportedEncodings.Add(Encoding.UTF8);

From f3029428cd118b34d73706fdd85cc3918b312a86 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 7 Jun 2020 15:33:54 +0200
Subject: [PATCH 151/463] Fix suggestions

---
 Jellyfin.Api/Controllers/SearchController.cs | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 15a650bf97..2ee3ef25b3 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -10,9 +10,9 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Search;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers
     /// Search controller.
     /// </summary>
     [Route("/Search/Hints")]
-    [Authenticated]
+    [Authorize]
     public class SearchController : BaseJellyfinApiController
     {
         private readonly ISearchEngine _searchEngine;
@@ -52,9 +52,9 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets the search hint result.
         /// </summary>
-        /// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param>
-        /// <param name="limit">The maximum number of records to return.</param>
-        /// <param name="userId">Supply a user id to search within a user's library or omit to search all.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
         /// <param name="searchTerm">The search term to filter on.</param>
         /// <param name="includePeople">Optional filter whether to include people.</param>
         /// <param name="includeMedia">Optional filter whether to include media.</param>
@@ -117,11 +117,11 @@ namespace Jellyfin.Api.Controllers
                 IsSports = isSports
             });
 
-            return Ok(new SearchHintResult
+            return new SearchHintResult
             {
                 TotalRecordCount = result.TotalRecordCount,
                 SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
-            });
+            };
         }
 
         /// <summary>

From 7fa374f8a2560cd1c58584b3d5b0567c91ef4138 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 7 Jun 2020 15:41:49 +0200
Subject: [PATCH 152/463] Move Split method from BaseJellyfinApiController.cs
 to RequestHelpers.cs

---
 Jellyfin.Api/BaseJellyfinApiController.cs    | 18 ------------
 Jellyfin.Api/Controllers/SearchController.cs |  7 +++--
 Jellyfin.Api/Helpers/RequestHelpers.cs       | 29 ++++++++++++++++++++
 3 files changed, 33 insertions(+), 21 deletions(-)
 create mode 100644 Jellyfin.Api/Helpers/RequestHelpers.cs

diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 6a9e48f8dd..22b9c3fd67 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -10,23 +10,5 @@ namespace Jellyfin.Api
     [Route("[controller]")]
     public class BaseJellyfinApiController : ControllerBase
     {
-        /// <summary>
-        /// Splits a string at a seperating character into an array of substrings.
-        /// </summary>
-        /// <param name="value">The string to split.</param>
-        /// <param name="separator">The char that seperates the substrings.</param>
-        /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
-        /// <returns>An array of the substrings.</returns>
-        internal static string[] Split(string value, char separator, bool removeEmpty)
-        {
-            if (string.IsNullOrWhiteSpace(value))
-            {
-                return Array.Empty<string>();
-            }
-
-            return removeEmpty
-                ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
-                : value.Split(separator);
-        }
     }
 }
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 2ee3ef25b3..b6178e121d 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -105,9 +106,9 @@ namespace Jellyfin.Api.Controllers
                 IncludeStudios = includeStudios,
                 StartIndex = startIndex,
                 UserId = userId,
-                IncludeItemTypes = Split(includeItemTypes, ',', true),
-                ExcludeItemTypes = Split(excludeItemTypes, ',', true),
-                MediaTypes = Split(mediaTypes, ',', true),
+                IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
                 ParentId = parentId,
 
                 IsKids = isKids,
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
new file mode 100644
index 0000000000..6c661e2d32
--- /dev/null
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Request Extensions.
+    /// </summary>
+    public static class RequestHelpers
+    {
+        /// <summary>
+        /// Splits a string at a seperating character into an array of substrings.
+        /// </summary>
+        /// <param name="value">The string to split.</param>
+        /// <param name="separator">The char that seperates the substrings.</param>
+        /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
+        /// <returns>An array of the substrings.</returns>
+        internal static string[] Split(string value, char separator, bool removeEmpty)
+        {
+            if (string.IsNullOrWhiteSpace(value))
+            {
+                return Array.Empty<string>();
+            }
+
+            return removeEmpty
+                ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+                : value.Split(separator);
+        }
+    }
+}

From cefa9d3c086fd01b6f05080fa272fcf1f76158f2 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 7 Jun 2020 18:10:08 +0200
Subject: [PATCH 153/463] Add default values for parameters and fix spelling

---
 Jellyfin.Api/Controllers/SearchController.cs | 22 ++++++++++----------
 Jellyfin.Api/Helpers/RequestHelpers.cs       |  4 ++--
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index b6178e121d..411c19a59b 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -57,11 +57,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
         /// <param name="searchTerm">The search term to filter on.</param>
-        /// <param name="includePeople">Optional filter whether to include people.</param>
-        /// <param name="includeMedia">Optional filter whether to include media.</param>
-        /// <param name="includeGenres">Optional filter whether to include genres.</param>
-        /// <param name="includeStudios">Optional filter whether to include studios.</param>
-        /// <param name="includeArtists">Optional filter whether to include artists.</param>
         /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
         /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
         /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
@@ -71,6 +66,11 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isNews">Optional filter for news.</param>
         /// <param name="isKids">Optional filter for kids.</param>
         /// <param name="isSports">Optional filter for sports.</param>
+        /// <param name="includePeople">Optional filter whether to include people.</param>
+        /// <param name="includeMedia">Optional filter whether to include media.</param>
+        /// <param name="includeGenres">Optional filter whether to include genres.</param>
+        /// <param name="includeStudios">Optional filter whether to include studios.</param>
+        /// <param name="includeArtists">Optional filter whether to include artists.</param>
         /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
         [HttpGet]
         [Description("Gets search hints based on a search term")]
@@ -80,11 +80,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] Guid userId,
             [FromQuery, Required] string searchTerm,
-            [FromQuery] bool includePeople,
-            [FromQuery] bool includeMedia,
-            [FromQuery] bool includeGenres,
-            [FromQuery] bool includeStudios,
-            [FromQuery] bool includeArtists,
             [FromQuery] string includeItemTypes,
             [FromQuery] string excludeItemTypes,
             [FromQuery] string mediaTypes,
@@ -93,7 +88,12 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isSeries,
             [FromQuery] bool? isNews,
             [FromQuery] bool? isKids,
-            [FromQuery] bool? isSports)
+            [FromQuery] bool? isSports,
+            [FromQuery] bool includePeople = true,
+            [FromQuery] bool includeMedia = true,
+            [FromQuery] bool includeGenres = true,
+            [FromQuery] bool includeStudios = true,
+            [FromQuery] bool includeArtists = true)
         {
             var result = _searchEngine.GetSearchHints(new SearchQuery
             {
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 6c661e2d32..9f4d34f9c6 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -8,10 +8,10 @@ namespace Jellyfin.Api.Helpers
     public static class RequestHelpers
     {
         /// <summary>
-        /// Splits a string at a seperating character into an array of substrings.
+        /// Splits a string at a separating character into an array of substrings.
         /// </summary>
         /// <param name="value">The string to split.</param>
-        /// <param name="separator">The char that seperates the substrings.</param>
+        /// <param name="separator">The char that separates the substrings.</param>
         /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
         /// <returns>An array of the substrings.</returns>
         internal static string[] Split(string value, char separator, bool removeEmpty)

From dd5579e0eb07202988a0800619e5df922c356f10 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 7 Jun 2020 22:04:59 -0600
Subject: [PATCH 154/463] Move FilterService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/FilterController.cs | 219 +++++++++++++++++
 MediaBrowser.Api/FilterService.cs            | 243 -------------------
 2 files changed, 219 insertions(+), 243 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/FilterController.cs
 delete mode 100644 MediaBrowser.Api/FilterService.cs

diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
new file mode 100644
index 0000000000..82fe207aef
--- /dev/null
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -0,0 +1,219 @@
+#nullable enable
+#pragma warning disable CA1801
+
+using System;
+using System.Linq;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Filters controller.
+    /// </summary>
+    [Authorize]
+    public class FilterController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FilterController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        public FilterController(ILibraryManager libraryManager, IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Gets legacy query filters.
+        /// </summary>
+        /// <param name="userId">Optional. User id.</param>
+        /// <param name="parentId">Optional. Parent id.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <returns>Legacy query filters.</returns>
+        [HttpGet("/Items/Filters")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
+            [FromQuery] Guid? userId,
+            [FromQuery] string? parentId,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? mediaTypes)
+        {
+            var parentItem = string.IsNullOrEmpty(parentId)
+                ? null
+                : _libraryManager.GetItemById(parentId);
+
+            var user = userId == null || userId == Guid.Empty
+                ? null
+                : _userManager.GetUserById(userId.Value);
+
+            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+            {
+                parentItem = null;
+            }
+
+            var item = string.IsNullOrEmpty(parentId)
+                ? user == null
+                    ? _libraryManager.RootFolder
+                    : _libraryManager.GetUserRootFolder()
+                : parentItem;
+
+            var query = new InternalItemsQuery
+            {
+                User = user,
+                MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                Recursive = true,
+                EnableTotalRecordCount = false,
+                DtoOptions = new DtoOptions
+                {
+                    Fields = new[] { ItemFields.Genres, ItemFields.Tags },
+                    EnableImages = false,
+                    EnableUserData = false
+                }
+            };
+
+            var itemList = ((Folder)item!).GetItemList(query);
+            return new QueryFiltersLegacy
+            {
+                Years = itemList.Select(i => i.ProductionYear ?? -1)
+                    .Where(i => i > 0)
+                    .Distinct()
+                    .OrderBy(i => i)
+                    .ToArray(),
+
+                Genres = itemList.SelectMany(i => i.Genres)
+                    .DistinctNames()
+                    .OrderBy(i => i)
+                    .ToArray(),
+
+                Tags = itemList
+                    .SelectMany(i => i.Tags)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .OrderBy(i => i)
+                    .ToArray(),
+
+                OfficialRatings = itemList
+                    .Select(i => i.OfficialRating)
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .OrderBy(i => i)
+                    .ToArray()
+            };
+        }
+
+        /// <summary>
+        /// Gets query filters.
+        /// </summary>
+        /// <param name="userId">Optional. User id.</param>
+        /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="mediaTypes">[Unused] Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="isAiring">Optional. Is item airing.</param>
+        /// <param name="isMovie">Optional. Is item movie.</param>
+        /// <param name="isSports">Optional. Is item sports.</param>
+        /// <param name="isKids">Optional. Is item kids.</param>
+        /// <param name="isNews">Optional. Is item news.</param>
+        /// <param name="isSeries">Optional. Is item series.</param>
+        /// <param name="recursive">Optional. Search recursive.</param>
+        /// <returns>Query filters.</returns>
+        [HttpGet("/Items/Filters2")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryFilters> GetQueryFilters(
+            [FromQuery] Guid? userId,
+            [FromQuery] string? parentId,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] bool? isAiring,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isSports,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? recursive)
+        {
+            var parentItem = string.IsNullOrEmpty(parentId)
+                ? null
+                : _libraryManager.GetItemById(parentId);
+
+            var user = userId == null || userId == Guid.Empty
+                ? null
+                : _userManager.GetUserById(userId.Value);
+
+            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+            {
+                parentItem = null;
+            }
+
+            var filters = new QueryFilters();
+            var genreQuery = new InternalItemsQuery(user)
+            {
+                IncludeItemTypes =
+                    (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                DtoOptions = new DtoOptions
+                {
+                    Fields = Array.Empty<ItemFields>(),
+                    EnableImages = false,
+                    EnableUserData = false
+                },
+                IsAiring = isAiring,
+                IsMovie = isMovie,
+                IsSports = isSports,
+                IsKids = isKids,
+                IsNews = isNews,
+                IsSeries = isSeries
+            };
+
+            if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
+            {
+                genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
+            }
+            else
+            {
+                genreQuery.Parent = parentItem;
+            }
+
+            if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
+            {
+                filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
+                {
+                    Name = i.Item1.Name,
+                    Id = i.Item1.Id
+                }).ToArray();
+            }
+            else
+            {
+                filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
+                {
+                    Name = i.Item1.Name,
+                    Id = i.Item1.Id
+                }).ToArray();
+            }
+
+            return filters;
+        }
+    }
+}
\ No newline at end of file
diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs
deleted file mode 100644
index 5eb72cdb19..0000000000
--- a/MediaBrowser.Api/FilterService.cs
+++ /dev/null
@@ -1,243 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Items/Filters", "GET", Summary = "Gets branding configuration")]
-    public class GetQueryFiltersLegacy : IReturn<QueryFiltersLegacy>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-    }
-
-    [Route("/Items/Filters2", "GET", Summary = "Gets branding configuration")]
-    public class GetQueryFilters : IReturn<QueryFilters>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public bool? IsAiring { get; set; }
-        public bool? IsMovie { get; set; }
-        public bool? IsSports { get; set; }
-        public bool? IsKids { get; set; }
-        public bool? IsNews { get; set; }
-        public bool? IsSeries { get; set; }
-        public bool? Recursive { get; set; }
-    }
-
-    [Authenticated]
-    public class FilterService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserManager _userManager;
-
-        public FilterService(
-            ILogger<FilterService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            IUserManager userManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-        }
-
-        public object Get(GetQueryFilters request)
-        {
-            var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
-            {
-                parentItem = null;
-            }
-
-            var filters = new QueryFilters();
-
-            var genreQuery = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = new ItemFields[] { },
-                    EnableImages = false,
-                    EnableUserData = false
-                },
-                IsAiring = request.IsAiring,
-                IsMovie = request.IsMovie,
-                IsSports = request.IsSports,
-                IsKids = request.IsKids,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries
-            };
-
-            // Non recursive not yet supported for library folders
-            if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
-            {
-                genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
-            }
-            else
-            {
-                genreQuery.Parent = parentItem;
-            }
-
-            if (string.Equals(request.IncludeItemTypes, "MusicAlbum", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "MusicVideo", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "MusicArtist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Audio", StringComparison.OrdinalIgnoreCase))
-            {
-                filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
-                {
-                    Name = i.Item1.Name,
-                    Id = i.Item1.Id
-
-                }).ToArray();
-            }
-            else
-            {
-                filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
-                {
-                    Name = i.Item1.Name,
-                    Id = i.Item1.Id
-
-                }).ToArray();
-            }
-
-            return ToOptimizedResult(filters);
-        }
-
-        public object Get(GetQueryFiltersLegacy request)
-        {
-            var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
-            {
-                parentItem = null;
-            }
-
-            var item = string.IsNullOrEmpty(request.ParentId) ?
-               user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
-               parentItem;
-
-            var result = ((Folder)item).GetItemList(GetItemsQuery(request, user));
-
-            var filters = GetFilters(result);
-
-            return ToOptimizedResult(filters);
-        }
-
-        private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
-        {
-            var result = new QueryFiltersLegacy();
-
-            result.Years = items.Select(i => i.ProductionYear ?? -1)
-                .Where(i => i > 0)
-                .Distinct()
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.Genres = items.SelectMany(i => i.Genres)
-                .DistinctNames()
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.Tags = items
-                .SelectMany(i => i.Tags)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.OfficialRatings = items
-                .Select(i => i.OfficialRating)
-                .Where(i => !string.IsNullOrWhiteSpace(i))
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
-                .ToArray();
-
-            return result;
-        }
-
-        private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, User user)
-        {
-            var query = new InternalItemsQuery
-            {
-                User = user,
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                Recursive = true,
-                EnableTotalRecordCount = false,
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = new[] { ItemFields.Genres, ItemFields.Tags },
-                    EnableImages = false,
-                    EnableUserData = false
-                }
-            };
-
-            return query;
-        }
-    }
-}

From 16e26be87f3c17422b7467882c5170fe11031825 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 07:22:40 -0600
Subject: [PATCH 155/463] Move ItemLookupService to Jellyfin.Api

---
 .../Controllers/ItemLookupController.cs       | 364 ++++++++++++++++++
 MediaBrowser.Api/ItemLookupService.cs         | 332 ----------------
 2 files changed, 364 insertions(+), 332 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ItemLookupController.cs
 delete mode 100644 MediaBrowser.Api/ItemLookupService.cs

diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
new file mode 100644
index 0000000000..e474f2b23d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -0,0 +1,364 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Item lookup controller.
+    /// </summary>
+    [Authorize]
+    public class ItemLookupController : BaseJellyfinApiController
+    {
+        private readonly IProviderManager _providerManager;
+        private readonly IServerApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILogger<ItemLookupController> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemLookupController"/> class.
+        /// </summary>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
+        public ItemLookupController(
+            IProviderManager providerManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager,
+            ILogger<ItemLookupController> logger)
+        {
+            _providerManager = providerManager;
+            _appPaths = serverConfigurationManager.ApplicationPaths;
+            _fileSystem = fileSystem;
+            _libraryManager = libraryManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Get the item's external id info.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">External id info retrieved.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>List of external id info.</returns>
+        [HttpGet("/Items/{itemId}/ExternalIdInfos")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute] Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return Ok(_providerManager.GetExternalIdInfos(item));
+        }
+
+        /// <summary>
+        /// Get movie remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Movie remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Movie")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MovieInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get trailer remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Trailer remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Trailer")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<TrailerInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get music video remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Music video remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/MusicVideo")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MusicVideoInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get series remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Series remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Series")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<SeriesInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get box set remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Box set remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/BoxSet")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BoxSetInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get music artist remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Music artist remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/MusicArtist")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<ArtistInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get music album remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Music album remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/MusicAlbum")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<AlbumInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get person remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Person remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Person")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<PersonLookupInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Get book remote search.
+        /// </summary>
+        /// <param name="query">Remote search query.</param>
+        /// <response code="200">Book remote search executed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Book")]
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BookInfo> query)
+        {
+            var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
+                .ConfigureAwait(false);
+            return Ok(results);
+        }
+
+        /// <summary>
+        /// Gets a remote image.
+        /// </summary>
+        /// <param name="imageUrl">The image url.</param>
+        /// <param name="providerName">The provider name.</param>
+        /// <response code="200">Remote image retrieved.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
+        /// </returns>
+        [HttpGet("/Items/RemoteSearch/Image")]
+        public async Task<ActionResult> GetRemoteSearchImage(
+            [FromQuery, Required] string imageUrl,
+            [FromQuery, Required] string providerName)
+        {
+            var urlHash = imageUrl.GetMD5();
+            var pointerCachePath = GetFullCachePath(urlHash.ToString());
+
+            try
+            {
+                var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+                if (System.IO.File.Exists(contentPath))
+                {
+                    await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
+                    return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+                // Means the file isn't cached yet
+            }
+            catch (IOException)
+            {
+                // Means the file isn't cached yet
+            }
+
+            await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
+
+            // Read the pointer file again
+            await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
+            return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
+        }
+
+        /// <summary>
+        /// Applies search criteria to an item and refreshes metadata.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="searchResult">The remote search result.</param>
+        /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
+        /// <response code="200">Item metadata refreshed.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
+        /// The task result contains an <see cref="OkResult"/>.
+        /// </returns>
+        [HttpPost("/Items/RemoteSearch/Apply/{id}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public async Task<ActionResult> ApplySearchCriteria(
+            [FromRoute] Guid itemId,
+            [FromBody, BindRequired] RemoteSearchResult searchResult,
+            [FromQuery] bool replaceAllImages = true)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            _logger.LogInformation(
+                "Setting provider id's to item {0}-{1}: {2}",
+                item.Id,
+                item.Name,
+                JsonSerializer.Serialize(searchResult.ProviderIds));
+
+            // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
+            item.ProviderIds = searchResult.ProviderIds;
+            await _providerManager.RefreshFullItem(
+                item,
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ReplaceAllMetadata = true,
+                    ReplaceAllImages = replaceAllImages,
+                    SearchResult = searchResult
+                }, CancellationToken.None).ConfigureAwait(false);
+
+            return Ok();
+        }
+
+        /// <summary>
+        /// Downloads the image.
+        /// </summary>
+        /// <param name="providerName">Name of the provider.</param>
+        /// <param name="url">The URL.</param>
+        /// <param name="urlHash">The URL hash.</param>
+        /// <param name="pointerCachePath">The pointer cache path.</param>
+        /// <returns>Task.</returns>
+        private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
+        {
+            var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+            var ext = result.ContentType.Split('/').Last();
+            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
+
+            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+            await using (var stream = result.Content)
+            {
+                await using var fileStream = new FileStream(
+                    fullCachePath,
+                    FileMode.Create,
+                    FileAccess.Write,
+                    FileShare.Read,
+                    IODefaults.FileStreamBufferSize,
+                    true);
+
+                await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+            await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the full cache path.
+        /// </summary>
+        /// <param name="filename">The filename.</param>
+        /// <returns>System.String.</returns>
+        private string GetFullCachePath(string filename)
+            => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
+    }
+}
\ No newline at end of file
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
deleted file mode 100644
index 0bbe7e1cfa..0000000000
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ /dev/null
@@ -1,332 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Items/{Id}/ExternalIdInfos", "GET", Summary = "Gets external id infos for an item")]
-    [Authenticated(Roles = "Admin")]
-    public class GetExternalIdInfos : IReturn<List<ExternalIdInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-    }
-
-    [Route("/Items/RemoteSearch/Movie", "POST")]
-    [Authenticated]
-    public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Trailer", "POST")]
-    [Authenticated]
-    public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicVideo", "POST")]
-    [Authenticated]
-    public class GetMusicVideoRemoteSearchResults : RemoteSearchQuery<MusicVideoInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Series", "POST")]
-    [Authenticated]
-    public class GetSeriesRemoteSearchResults : RemoteSearchQuery<SeriesInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/BoxSet", "POST")]
-    [Authenticated]
-    public class GetBoxSetRemoteSearchResults : RemoteSearchQuery<BoxSetInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicArtist", "POST")]
-    [Authenticated]
-    public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery<ArtistInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicAlbum", "POST")]
-    [Authenticated]
-    public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery<AlbumInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Person", "POST")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPersonRemoteSearchResults : RemoteSearchQuery<PersonLookupInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Book", "POST")]
-    [Authenticated]
-    public class GetBookRemoteSearchResults : RemoteSearchQuery<BookInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")]
-    public class GetRemoteSearchImage
-    {
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ImageUrl { get; set; }
-
-        [ApiMember(Name = "ProviderName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProviderName { get; set; }
-    }
-
-    [Route("/Items/RemoteSearch/Apply/{Id}", "POST", Summary = "Applies search criteria to an item and refreshes metadata")]
-    [Authenticated(Roles = "Admin")]
-    public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "ReplaceAllImages", Description = "Whether or not to replace all images", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool ReplaceAllImages { get; set; }
-
-        public ApplySearchCriteria()
-        {
-            ReplaceAllImages = true;
-        }
-    }
-
-    public class ItemLookupService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IJsonSerializer _json;
-
-        public ItemLookupService(
-            ILogger<ItemLookupService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager,
-            IJsonSerializer json)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-            _json = json;
-        }
-
-        public object Get(GetExternalIdInfos request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var infos = _providerManager.GetExternalIdInfos(item).ToList();
-
-            return ToOptimizedResult(infos);
-        }
-
-        public async Task<object> Post(GetTrailerRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetBookRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMovieRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetSeriesRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetBoxSetRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicVideoRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetPersonRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicAlbumRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicArtistRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task<object> Get(GetRemoteSearchImage request)
-        {
-            return GetRemoteImage(request);
-        }
-
-        public Task Post(ApplySearchCriteria request)
-        {
-            var item = _libraryManager.GetItemById(new Guid(request.Id));
-
-            //foreach (var key in request.ProviderIds)
-            //{
-            //    var value = key.Value;
-
-            //    if (!string.IsNullOrWhiteSpace(value))
-            //    {
-            //        item.SetProviderId(key.Key, value);
-            //    }
-            //}
-            Logger.LogInformation("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds));
-
-            // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
-            item.ProviderIds = request.ProviderIds;
-            //item.ProductionYear = request.ProductionYear;
-            //item.Name = request.Name;
-
-            return _providerManager.RefreshFullItem(
-                item,
-                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                {
-                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ReplaceAllMetadata = true,
-                    ReplaceAllImages = request.ReplaceAllImages,
-                    SearchResult = request
-                },
-                CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Gets the remote image.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetRemoteImage(GetRemoteSearchImage request)
-        {
-            var urlHash = request.ImageUrl.GetMD5();
-            var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
-            string contentPath;
-
-            try
-            {
-                contentPath = File.ReadAllText(pointerCachePath);
-
-                if (File.Exists(contentPath))
-                {
-                    return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // Means the file isn't cached yet
-            }
-            catch (IOException)
-            {
-                // Means the file isn't cached yet
-            }
-
-            await DownloadImage(request.ProviderName, request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
-            // Read the pointer file again
-            contentPath = File.ReadAllText(pointerCachePath);
-
-            return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Downloads the image.
-        /// </summary>
-        /// <param name="providerName">Name of the provider.</param>
-        /// <param name="url">The URL.</param>
-        /// <param name="urlHash">The URL hash.</param>
-        /// <param name="pointerCachePath">The pointer cache path.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
-        {
-            var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
-
-            var ext = result.ContentType.Split('/').Last();
-
-            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            using (var stream = result.Content)
-            {
-                using var fileStream = new FileStream(
-                    fullCachePath,
-                    FileMode.Create,
-                    FileAccess.Write,
-                    FileShare.Read,
-                    IODefaults.FileStreamBufferSize,
-                    true);
-
-                await stream.CopyToAsync(fileStream).ConfigureAwait(false);
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
-            File.WriteAllText(pointerCachePath, fullCachePath);
-        }
-
-        /// <summary>
-        /// Gets the full cache path.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        private string GetFullCachePath(string filename)
-            => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-    }
-}

From 6f64e48c540db93053961a028b95e8f0bbb64076 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 07:37:16 -0600
Subject: [PATCH 156/463] Add response codes

---
 Jellyfin.Api/Controllers/FilterController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 82fe207aef..d06c5e96c9 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="parentId">Optional. Parent id.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <response code="200">Legacy filters retrieved.</response>
         /// <returns>Legacy query filters.</returns>
         [HttpGet("/Items/Filters")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isNews">Optional. Is item news.</param>
         /// <param name="isSeries">Optional. Is item series.</param>
         /// <param name="recursive">Optional. Search recursive.</param>
+        /// <response code="200">Filters retrieved.</response>
         /// <returns>Query filters.</returns>
         [HttpGet("/Items/Filters2")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 81d3ec7205dd559e6444c17f9ecefc37977a5911 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 12:20:33 -0600
Subject: [PATCH 157/463] Move ItemUpdateService to Jellyfin.Api

---
 .../Controllers/ItemUpdateController.cs       | 423 ++++++++++--------
 1 file changed, 237 insertions(+), 186 deletions(-)
 rename MediaBrowser.Api/ItemUpdateService.cs => Jellyfin.Api/Controllers/ItemUpdateController.cs (63%)

diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
similarity index 63%
rename from MediaBrowser.Api/ItemUpdateService.cs
rename to Jellyfin.Api/Controllers/ItemUpdateController.cs
index 2db6d717aa..0c5fece832 100644
--- a/MediaBrowser.Api/ItemUpdateService.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,215 +1,101 @@
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
 
-namespace MediaBrowser.Api
+namespace Jellyfin.Api.Controllers
 {
-    [Route("/Items/{ItemId}", "POST", Summary = "Updates an item")]
-    public class UpdateItem : BaseItemDto, IReturnVoid
-    {
-        [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string ItemId { get; set; }
-    }
-
-    [Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")]
-    public class GetMetadataEditorInfo : IReturn<MetadataEditorInfo>
-    {
-        [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string ItemId { get; set; }
-    }
-
-    [Route("/Items/{ItemId}/ContentType", "POST", Summary = "Updates an item's content type")]
-    public class UpdateItemContentType : IReturnVoid
-    {
-        [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid ItemId { get; set; }
-
-        [ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ContentType { get; set; }
-    }
-
-    [Authenticated(Roles = "admin")]
-    public class ItemUpdateService : BaseApiService
+    /// <summary>
+    /// Item update controller.
+    /// </summary>
+    [Authorize(Policy = Policies.RequiresElevation)]
+    public class ItemUpdateController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IProviderManager _providerManager;
         private readonly ILocalizationManager _localizationManager;
         private readonly IFileSystem _fileSystem;
-
-        public ItemUpdateService(
-            ILogger<ItemUpdateService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
+        /// </summary>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public ItemUpdateController(
             IFileSystem fileSystem,
             ILibraryManager libraryManager,
             IProviderManager providerManager,
-            ILocalizationManager localizationManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
+            ILocalizationManager localizationManager,
+            IServerConfigurationManager serverConfigurationManager)
         {
             _libraryManager = libraryManager;
             _providerManager = providerManager;
             _localizationManager = localizationManager;
             _fileSystem = fileSystem;
+            _serverConfigurationManager = serverConfigurationManager;
         }
 
-        public object Get(GetMetadataEditorInfo request)
+        /// <summary>
+        /// Updates an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="request">The new item properties.</param>
+        /// <response code="200">Item updated.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        [HttpPost("/Items/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request)
         {
-            var item = _libraryManager.GetItemById(request.ItemId);
-
-            var info = new MetadataEditorInfo
-            {
-                ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
-                ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
-                Countries = _localizationManager.GetCountries().ToArray(),
-                Cultures = _localizationManager.GetCultures().ToArray()
-            };
-
-            if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) &&
-                item.SourceType == SourceType.Library)
-            {
-                var inheritedContentType = _libraryManager.GetInheritedContentType(item);
-                var configuredContentType = _libraryManager.GetConfiguredContentType(item);
-
-                if (string.IsNullOrWhiteSpace(inheritedContentType) || !string.IsNullOrWhiteSpace(configuredContentType))
-                {
-                    info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
-                    info.ContentType = configuredContentType;
-
-                    if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
-                    {
-                        info.ContentTypeOptions = info.ContentTypeOptions
-                            .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
-                            .ToArray();
-                    }
-                }
-            }
-
-            return ToOptimizedResult(info);
-        }
-
-        public void Post(UpdateItemContentType request)
-        {
-            var item = _libraryManager.GetItemById(request.ItemId);
-            var path = item.ContainingFolderPath;
-
-            var types = ServerConfigurationManager.Configuration.ContentTypes
-                .Where(i => !string.IsNullOrWhiteSpace(i.Name))
-                .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
-                .ToList();
-
-            if (!string.IsNullOrWhiteSpace(request.ContentType))
-            {
-                types.Add(new NameValuePair
-                {
-                    Name = path,
-                    Value = request.ContentType
-                });
-            }
-
-            ServerConfigurationManager.Configuration.ContentTypes = types.ToArray();
-            ServerConfigurationManager.SaveConfiguration();
-        }
-
-        private List<NameValuePair> GetContentTypeOptions(bool isForItem)
-        {
-            var list = new List<NameValuePair>();
-
-            if (isForItem)
-            {
-                list.Add(new NameValuePair
-                {
-                    Name = "Inherit",
-                    Value = ""
-                });
-            }
-
-            list.Add(new NameValuePair
-            {
-                Name = "Movies",
-                Value = "movies"
-            });
-            list.Add(new NameValuePair
-            {
-                Name = "Music",
-                Value = "music"
-            });
-            list.Add(new NameValuePair
-            {
-                Name = "Shows",
-                Value = "tvshows"
-            });
-
-            if (!isForItem)
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
             {
-                list.Add(new NameValuePair
-                {
-                    Name = "Books",
-                    Value = "books"
-                });
+                return NotFound();
             }
 
-            list.Add(new NameValuePair
-            {
-                Name = "HomeVideos",
-                Value = "homevideos"
-            });
-            list.Add(new NameValuePair
-            {
-                Name = "MusicVideos",
-                Value = "musicvideos"
-            });
-            list.Add(new NameValuePair
-            {
-                Name = "Photos",
-                Value = "photos"
-            });
-
-            if (!isForItem)
-            {
-                list.Add(new NameValuePair
-                {
-                    Name = "MixedContent",
-                    Value = ""
-                });
-            }
-
-            foreach (var val in list)
-            {
-                val.Name = _localizationManager.GetLocalizedString(val.Name);
-            }
-
-            return list;
-        }
-
-        public void Post(UpdateItem request)
-        {
-            var item = _libraryManager.GetItemById(request.ItemId);
-
             var newLockData = request.LockData ?? false;
             var isLockedChanged = item.IsLocked != newLockData;
 
             var series = item as Series;
-            var displayOrderChanged = series != null && !string.Equals(series.DisplayOrder ?? string.Empty, request.DisplayOrder ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+            var displayOrderChanged = series != null && !string.Equals(
+                series.DisplayOrder ?? string.Empty,
+                request.DisplayOrder ?? string.Empty,
+                StringComparison.OrdinalIgnoreCase);
 
             // Do this first so that metadata savers can pull the updates from the database.
             if (request.People != null)
             {
-                _libraryManager.UpdatePeople(item, request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList());
+                _libraryManager.UpdatePeople(
+                    item,
+                    request.People.Select(x => new PersonInfo
+                    {
+                        Name = x.Name,
+                        Role = x.Role,
+                        Type = x.Type
+                    }).ToList());
             }
 
             UpdateItem(request, item);
@@ -232,7 +118,7 @@ namespace MediaBrowser.Api
             if (displayOrderChanged)
             {
                 _providerManager.QueueRefresh(
-                    series.Id,
+                    series!.Id,
                     new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
@@ -241,11 +127,99 @@ namespace MediaBrowser.Api
                     },
                     RefreshPriority.High);
             }
+
+            return Ok();
         }
 
-        private DateTime NormalizeDateTime(DateTime val)
+        /// <summary>
+        /// Gets metadata editor info for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="200">Item metadata editor returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        [HttpGet("/Items/{itemId}/MetadataEditor")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId)
         {
-            return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+            var item = _libraryManager.GetItemById(itemId);
+
+            var info = new MetadataEditorInfo
+            {
+                ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
+                ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
+                Countries = _localizationManager.GetCountries().ToArray(),
+                Cultures = _localizationManager.GetCultures().ToArray()
+            };
+
+            if (!item.IsVirtualItem
+                && !(item is ICollectionFolder)
+                && !(item is UserView)
+                && !(item is AggregateFolder)
+                && !(item is LiveTvChannel)
+                && !(item is IItemByName)
+                && item.SourceType == SourceType.Library)
+            {
+                var inheritedContentType = _libraryManager.GetInheritedContentType(item);
+                var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+
+                if (string.IsNullOrWhiteSpace(inheritedContentType) ||
+                    !string.IsNullOrWhiteSpace(configuredContentType))
+                {
+                    info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
+                    info.ContentType = configuredContentType;
+
+                    if (string.IsNullOrWhiteSpace(inheritedContentType)
+                        || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+                    {
+                        info.ContentTypeOptions = info.ContentTypeOptions
+                            .Where(i => string.IsNullOrWhiteSpace(i.Value)
+                                        || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+                            .ToArray();
+                    }
+                }
+            }
+
+            return info;
+        }
+
+        /// <summary>
+        /// Updates an item's content type.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="contentType">The content type of the item.</param>
+        /// <response code="200">Item content type updated.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        [HttpPost("/Items/{itemId}/ContentType")]
+        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            var path = item.ContainingFolderPath;
+
+            var types = _serverConfigurationManager.Configuration.ContentTypes
+                .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+                .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
+                .ToList();
+
+            if (!string.IsNullOrWhiteSpace(contentType))
+            {
+                types.Add(new NameValuePair
+                {
+                    Name = path,
+                    Value = contentType
+                });
+            }
+
+            _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
+            _serverConfigurationManager.SaveConfiguration();
+            return Ok();
         }
 
         private void UpdateItem(BaseItemDto request, BaseItem item)
@@ -361,24 +335,25 @@ namespace MediaBrowser.Api
                 }
             }
 
-            if (item is Audio song)
-            {
-                song.Album = request.Album;
-            }
-
-            if (item is MusicVideo musicVideo)
+            switch (item)
             {
-                musicVideo.Album = request.Album;
-            }
+                case Audio song:
+                    song.Album = request.Album;
+                    break;
+                case MusicVideo musicVideo:
+                    musicVideo.Album = request.Album;
+                    break;
+                case Series series:
+                {
+                    series.Status = GetSeriesStatus(request);
 
-            if (item is Series series)
-            {
-                series.Status = GetSeriesStatus(request);
+                    if (request.AirDays != null)
+                    {
+                        series.AirDays = request.AirDays;
+                        series.AirTime = request.AirTime;
+                    }
 
-                if (request.AirDays != null)
-                {
-                    series.AirDays = request.AirDays;
-                    series.AirTime = request.AirTime;
+                    break;
                 }
             }
         }
@@ -392,5 +367,81 @@ namespace MediaBrowser.Api
 
             return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
         }
+
+        private DateTime NormalizeDateTime(DateTime val)
+        {
+            return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+        }
+
+        private List<NameValuePair> GetContentTypeOptions(bool isForItem)
+        {
+            var list = new List<NameValuePair>();
+
+            if (isForItem)
+            {
+                list.Add(new NameValuePair
+                {
+                    Name = "Inherit",
+                    Value = string.Empty
+                });
+            }
+
+            list.Add(new NameValuePair
+            {
+                Name = "Movies",
+                Value = "movies"
+            });
+            list.Add(new NameValuePair
+            {
+                Name = "Music",
+                Value = "music"
+            });
+            list.Add(new NameValuePair
+            {
+                Name = "Shows",
+                Value = "tvshows"
+            });
+
+            if (!isForItem)
+            {
+                list.Add(new NameValuePair
+                {
+                    Name = "Books",
+                    Value = "books"
+                });
+            }
+
+            list.Add(new NameValuePair
+            {
+                Name = "HomeVideos",
+                Value = "homevideos"
+            });
+            list.Add(new NameValuePair
+            {
+                Name = "MusicVideos",
+                Value = "musicvideos"
+            });
+            list.Add(new NameValuePair
+            {
+                Name = "Photos",
+                Value = "photos"
+            });
+
+            if (!isForItem)
+            {
+                list.Add(new NameValuePair
+                {
+                    Name = "MixedContent",
+                    Value = string.Empty
+                });
+            }
+
+            foreach (var val in list)
+            {
+                val.Name = _localizationManager.GetLocalizedString(val.Name);
+            }
+
+            return list;
+        }
     }
 }

From a4455af3e90b440defb4da6de980f287b8a348c6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 12:34:17 -0600
Subject: [PATCH 158/463] Move LocalizationService to Jellyfin.Api

---
 .../Controllers/LocalizationController.cs     |  76 ++++++++++++
 MediaBrowser.Api/LocalizationService.cs       | 111 ------------------
 2 files changed, 76 insertions(+), 111 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/LocalizationController.cs
 delete mode 100644 MediaBrowser.Api/LocalizationService.cs

diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
new file mode 100644
index 0000000000..1466dd3ec0
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Localization controller.
+    /// </summary>
+    [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+    public class LocalizationController : BaseJellyfinApiController
+    {
+        private readonly ILocalizationManager _localization;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalizationController"/> class.
+        /// </summary>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        public LocalizationController(ILocalizationManager localization)
+        {
+            _localization = localization;
+        }
+
+        /// <summary>
+        /// Gets known cultures.
+        /// </summary>
+        /// <response code="200">Known cultures returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
+        [HttpGet("Cultures")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<CultureDto>> GetCultures()
+        {
+            return Ok(_localization.GetCultures());
+        }
+
+        /// <summary>
+        /// Gets known countries.
+        /// </summary>
+        /// <response code="200">Known countries returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
+        [HttpGet("Countries")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<CountryInfo>> GetCountries()
+        {
+            return Ok(_localization.GetCountries());
+        }
+
+        /// <summary>
+        /// Gets known parental ratings.
+        /// </summary>
+        /// <response code="200">Known parental ratings returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
+        [HttpGet("ParentalRatings")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
+        {
+            return Ok(_localization.GetParentalRatings());
+        }
+
+        /// <summary>
+        /// Gets localization options.
+        /// </summary>
+        /// <response code="200">Localization options returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
+        [HttpGet("Options")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
+        {
+            return Ok(_localization.GetLocalizationOptions());
+        }
+    }
+}
diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs
deleted file mode 100644
index 6a69d26568..0000000000
--- a/MediaBrowser.Api/LocalizationService.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetCultures
-    /// </summary>
-    [Route("/Localization/Cultures", "GET", Summary = "Gets known cultures")]
-    public class GetCultures : IReturn<CultureDto[]>
-    {
-    }
-
-    /// <summary>
-    /// Class GetCountries
-    /// </summary>
-    [Route("/Localization/Countries", "GET", Summary = "Gets known countries")]
-    public class GetCountries : IReturn<CountryInfo[]>
-    {
-    }
-
-    /// <summary>
-    /// Class ParentalRatings
-    /// </summary>
-    [Route("/Localization/ParentalRatings", "GET", Summary = "Gets known parental ratings")]
-    public class GetParentalRatings : IReturn<ParentalRating[]>
-    {
-    }
-
-    /// <summary>
-    /// Class ParentalRatings
-    /// </summary>
-    [Route("/Localization/Options", "GET", Summary = "Gets localization options")]
-    public class GetLocalizationOptions : IReturn<LocalizationOption[]>
-    {
-    }
-
-    /// <summary>
-    /// Class CulturesService
-    /// </summary>
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class LocalizationService : BaseApiService
-    {
-        /// <summary>
-        /// The _localization
-        /// </summary>
-        private readonly ILocalizationManager _localization;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LocalizationService"/> class.
-        /// </summary>
-        /// <param name="localization">The localization.</param>
-        public LocalizationService(
-            ILogger<LocalizationService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILocalizationManager localization)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _localization = localization;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetParentalRatings request)
-        {
-            var result = _localization.GetParentalRatings();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetLocalizationOptions request)
-        {
-            var result = _localization.GetLocalizationOptions();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCountries request)
-        {
-            var result = _localization.GetCountries();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCultures request)
-        {
-            var result = _localization.GetCultures();
-
-            return ToOptimizedResult(result);
-        }
-    }
-
-}

From d97e306cdae11eb2675161aa2a6d828c739b2b01 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 13:14:41 -0600
Subject: [PATCH 159/463] Move PlaylistService to Jellyfin.Api

---
 .../Controllers/PlaylistsController.cs        | 198 ++++++++++++++++++
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  30 +++
 .../Models/PlaylistDtos/CreatePlaylistDto.cs  |  31 +++
 MediaBrowser.Api/PlaylistService.cs           |  74 -------
 4 files changed, 259 insertions(+), 74 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/PlaylistsController.cs
 create mode 100644 Jellyfin.Api/Helpers/RequestHelpers.cs
 create mode 100644 Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs

diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
new file mode 100644
index 0000000000..0d73962de4
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -0,0 +1,198 @@
+#nullable enable
+#pragma warning disable CA1801
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaylistDtos;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Playlists;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Playlists controller.
+    /// </summary>
+    [Authorize]
+    public class PlaylistsController : BaseJellyfinApiController
+    {
+        private readonly IPlaylistManager _playlistManager;
+        private readonly IDtoService _dtoService;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
+        /// </summary>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public PlaylistsController(
+            IDtoService dtoService,
+            IPlaylistManager playlistManager,
+            IUserManager userManager,
+            ILibraryManager libraryManager)
+        {
+            _dtoService = dtoService;
+            _playlistManager = playlistManager;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Creates a new playlist.
+        /// </summary>
+        /// <param name="createPlaylistRequest">The create playlist payload.</param>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
+        /// The task result contains an <see cref="OkResult"/> indicating success.
+        /// </returns>
+        [HttpPost]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
+            [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest)
+        {
+            Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
+            var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
+            {
+                Name = createPlaylistRequest.Name,
+                ItemIdList = idGuidArray,
+                UserId = createPlaylistRequest.UserId,
+                MediaType = createPlaylistRequest.MediaType
+            }).ConfigureAwait(false);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Adds items to a playlist.
+        /// </summary>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="ids">Item id, comma delimited.</param>
+        /// <param name="userId">The userId.</param>
+        /// <response code="200">Items added to playlist.</response>
+        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        [HttpPost("{playlistId}/Items")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult AddToPlaylist(
+            [FromRoute] string playlistId,
+            [FromQuery] string ids,
+            [FromQuery] Guid userId)
+        {
+            _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Moves a playlist item.
+        /// </summary>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="newIndex">The new index.</param>
+        /// <response code="200">Item moved to new index.</response>
+        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult MoveItem(
+            [FromRoute] string playlistId,
+            [FromRoute] string itemId,
+            [FromRoute] int newIndex)
+        {
+            _playlistManager.MoveItem(playlistId, itemId, newIndex);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Removes items from a playlist.
+        /// </summary>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="entryIds">The item ids, comma delimited.</param>
+        /// <response code="200">Items removed.</response>
+        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        [HttpDelete("{playlistId}/Items")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds)
+        {
+            _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(','));
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets the original items of a playlist.
+        /// </summary>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Original playlist returned.</response>
+        /// <response code="404">Playlist not found.</response>
+        /// <returns>The original playlist items.</returns>
+        [HttpGet("{playlistId}/Items")]
+        public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
+            [FromRoute] Guid playlistId,
+            [FromRoute] Guid userId,
+            [FromRoute] int? startIndex,
+            [FromRoute] int? limit,
+            [FromRoute] string fields,
+            [FromRoute] bool? enableImages,
+            [FromRoute] bool? enableUserData,
+            [FromRoute] bool? imageTypeLimit,
+            [FromRoute] string enableImageTypes)
+        {
+            var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
+            if (playlist == null)
+            {
+                return NotFound();
+            }
+
+            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+            var items = playlist.GetManageableItems().ToArray();
+
+            var count = items.Length;
+
+            if (startIndex.HasValue)
+            {
+                items = items.Skip(startIndex.Value).ToArray();
+            }
+
+            if (limit.HasValue)
+            {
+                items = items.Take(limit.Value).ToArray();
+            }
+
+            // TODO var dtoOptions = GetDtoOptions(_authContext, request);
+            var dtoOptions = new DtoOptions();
+
+            var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
+
+            for (int index = 0; index < dtos.Count; index++)
+            {
+                dtos[index].PlaylistItemId = items[index].Item1.Id;
+            }
+
+            var result = new QueryResult<BaseItemDto>
+            {
+                Items = dtos,
+                TotalRecordCount = count
+            };
+
+            return result;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
new file mode 100644
index 0000000000..b1c6a24d0e
--- /dev/null
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -0,0 +1,30 @@
+#nullable enable
+
+using System;
+using System.Linq;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Request Helpers.
+    /// </summary>
+    public static class RequestHelpers
+    {
+        /// <summary>
+        /// Get Guid array from string.
+        /// </summary>
+        /// <param name="value">String value.</param>
+        /// <returns>Guid array.</returns>
+        public static Guid[] GetGuids(string? value)
+        {
+            if (value == null)
+            {
+                return Array.Empty<Guid>();
+            }
+
+            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(i => new Guid(i))
+                .ToArray();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
new file mode 100644
index 0000000000..20835eecbd
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -0,0 +1,31 @@
+#nullable enable
+using System;
+
+namespace Jellyfin.Api.Models.PlaylistDtos
+{
+    /// <summary>
+    /// Create new playlist dto.
+    /// </summary>
+    public class CreatePlaylistDto
+    {
+        /// <summary>
+        /// Gets or sets the name of the new playlist.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets item ids to add to the playlist.
+        /// </summary>
+        public string? Ids { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the media type.
+        /// </summary>
+        public string? MediaType { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs
index 953b00e35a..f4fa8955b7 100644
--- a/MediaBrowser.Api/PlaylistService.cs
+++ b/MediaBrowser.Api/PlaylistService.cs
@@ -14,66 +14,6 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api
 {
-    [Route("/Playlists", "POST", Summary = "Creates a new playlist")]
-    public class CreatePlaylist : IReturn<PlaylistCreationResult>
-    {
-        [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaType { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")]
-    public class AddToPlaylist : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")]
-    public class MoveItem : IReturnVoid
-    {
-        [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public int NewIndex { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
-    public class RemoveFromPlaylist : IReturnVoid
-    {
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string EntryIds { get; set; }
-    }
-
     [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
     public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
     {
@@ -153,20 +93,6 @@ namespace MediaBrowser.Api
             _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex);
         }
 
-        public async Task<object> Post(CreatePlaylist request)
-        {
-            var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
-            {
-                Name = request.Name,
-                ItemIdList = GetGuids(request.Ids),
-                UserId = request.UserId,
-                MediaType = request.MediaType
-
-            }).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
         public void Post(AddToPlaylist request)
         {
             _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId);

From 7a77b9928f2c8326e85629d3c900e86c3b26342a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 8 Jun 2020 13:14:55 -0600
Subject: [PATCH 160/463] Move PlaylistService to Jellyfin.Api

---
 MediaBrowser.Api/PlaylistService.cs | 143 ----------------------------
 1 file changed, 143 deletions(-)
 delete mode 100644 MediaBrowser.Api/PlaylistService.cs

diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs
deleted file mode 100644
index f4fa8955b7..0000000000
--- a/MediaBrowser.Api/PlaylistService.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Playlists;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
-    public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-    }
-
-    [Authenticated]
-    public class PlaylistService : BaseApiService
-    {
-        private readonly IPlaylistManager _playlistManager;
-        private readonly IDtoService _dtoService;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public PlaylistService(
-            ILogger<PlaylistService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDtoService dtoService,
-            IPlaylistManager playlistManager,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _dtoService = dtoService;
-            _playlistManager = playlistManager;
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _authContext = authContext;
-        }
-
-        public void Post(MoveItem request)
-        {
-            _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex);
-        }
-
-        public void Post(AddToPlaylist request)
-        {
-            _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId);
-        }
-
-        public void Delete(RemoveFromPlaylist request)
-        {
-            _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
-        }
-
-        public object Get(GetPlaylistItems request)
-        {
-            var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var items = playlist.GetManageableItems().ToArray();
-
-            var count = items.Length;
-
-            if (request.StartIndex.HasValue)
-            {
-                items = items.Skip(request.StartIndex.Value).ToArray();
-            }
-
-            if (request.Limit.HasValue)
-            {
-                items = items.Take(request.Limit.Value).ToArray();
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
-
-            for (int index = 0; index < dtos.Count; index++)
-            {
-                dtos[index].PlaylistItemId = items[index].Item1.Id;
-            }
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = count
-            };
-
-            return ToOptimizedResult(result);
-        }
-    }
-}

From 1491e93d841a8c40ea1a9fed9d99427a64ccd66c Mon Sep 17 00:00:00 2001
From: David Ullmer <daullmer@gmail.com>
Date: Tue, 9 Jun 2020 11:00:37 +0200
Subject: [PATCH 161/463] Add response code documentation

---
 Jellyfin.Api/Controllers/SearchController.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 411c19a59b..ec05e4fb4f 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -71,6 +71,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="includeGenres">Optional filter whether to include genres.</param>
         /// <param name="includeStudios">Optional filter whether to include studios.</param>
         /// <param name="includeArtists">Optional filter whether to include artists.</param>
+        /// <response code="200">Search hint returned.</response>
         /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
         [HttpGet]
         [Description("Gets search hints based on a search term")]

From 0b778b88516e59aafeaf5e6b8b0a117e0dd68607 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 9 Jun 2020 09:57:01 -0600
Subject: [PATCH 162/463] Move PluginService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/PluginsController.cs | 189 ++++++++++++
 .../Models/PluginDtos/MBRegistrationRecord.cs |  42 +++
 .../Models/PluginDtos/PluginSecurityInfo.cs   |  20 ++
 MediaBrowser.Api/PluginService.cs             | 268 ------------------
 4 files changed, 251 insertions(+), 268 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/PluginsController.cs
 create mode 100644 Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
 create mode 100644 Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
 delete mode 100644 MediaBrowser.Api/PluginService.cs

diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
new file mode 100644
index 0000000000..59196a41aa
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -0,0 +1,189 @@
+#nullable enable
+#pragma warning disable CA1801
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.PluginDtos;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Plugins controller.
+    /// </summary>
+    [Authorize]
+    public class PluginsController : BaseJellyfinApiController
+    {
+        private readonly IApplicationHost _appHost;
+        private readonly IInstallationManager _installationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PluginsController"/> class.
+        /// </summary>
+        /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
+        /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+        public PluginsController(
+            IApplicationHost appHost,
+            IInstallationManager installationManager)
+        {
+            _appHost = appHost;
+            _installationManager = installationManager;
+        }
+
+        /// <summary>
+        /// Gets a list of currently installed plugins.
+        /// </summary>
+        /// <param name="isAppStoreEnabled">Optional. Unused.</param>
+        /// <response code="200">Installed plugins returned.</response>
+        /// <returns>List of currently installed plugins.</returns>
+        [HttpGet]
+        public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
+        {
+            return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
+        }
+
+        /// <summary>
+        /// Uninstalls a plugin.
+        /// </summary>
+        /// <param name="pluginId">Plugin id.</param>
+        /// <response code="200">Plugin uninstalled.</response>
+        /// <response code="404">Plugin not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+        [HttpDelete("{pluginId}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
+        {
+            var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
+            if (plugin == null)
+            {
+                return NotFound();
+            }
+
+            _installationManager.UninstallPlugin(plugin);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets plugin configuration.
+        /// </summary>
+        /// <param name="pluginId">Plugin id.</param>
+        /// <response code="200">Plugin configuration returned.</response>
+        /// <response code="404">Plugin not found or plugin configuration not found.</response>
+        /// <returns>Plugin configuration.</returns>
+        [HttpGet("{pluginId}/Configuration")]
+        public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId)
+        {
+            if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+            {
+                return NotFound();
+            }
+
+            return plugin.Configuration;
+        }
+
+        /// <summary>
+        /// Updates plugin configuration.
+        /// </summary>
+        /// <remarks>
+        /// Accepts plugin configuration as JSON body.
+        /// </remarks>
+        /// <param name="pluginId">Plugin id.</param>
+        /// <response code="200">Plugin configuration updated.</response>
+        /// <response code="200">Plugin not found or plugin does not have configuration.</response>
+        /// <returns>
+        /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
+        ///    The task result contains an <see cref="OkResult"/> indicating success, or <see cref="NotFoundResult"/>
+        ///    when plugin not found or plugin doesn't have configuration.
+        /// </returns>
+        [HttpPost("{pluginId}/Configuration")]
+        public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
+        {
+            if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+            {
+                return NotFound();
+            }
+
+            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType)
+                .ConfigureAwait(false);
+
+            plugin.UpdateConfiguration(configuration);
+            return Ok();
+        }
+
+        /// <summary>
+        /// Get plugin security info.
+        /// </summary>
+        /// <response code="200">Plugin security info returned.</response>
+        /// <returns>Plugin security info.</returns>
+        [Obsolete("This endpoint should not be used.")]
+        [HttpGet("SecurityInfo")]
+        public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
+        {
+            return new PluginSecurityInfo
+            {
+                IsMbSupporter = true,
+                SupporterKey = "IAmTotallyLegit"
+            };
+        }
+
+        /// <summary>
+        /// Updates plugin security info.
+        /// </summary>
+        /// <param name="pluginSecurityInfo">Plugin security info.</param>
+        /// <response code="200">Plugin security info updated.</response>
+        /// <returns>An <see cref="OkResult"/>.</returns>
+        [Obsolete("This endpoint should not be used.")]
+        [HttpPost("SecurityInfo")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
+        {
+            return Ok();
+        }
+
+        /// <summary>
+        /// Gets registration status for a feature.
+        /// </summary>
+        /// <param name="name">Feature name.</param>
+        /// <response code="200">Registration status returned.</response>
+        /// <returns>Mb registration record.</returns>
+        [Obsolete("This endpoint should not be used.")]
+        [HttpPost("RegistrationRecords/{name}")]
+        public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name)
+        {
+            return new MBRegistrationRecord
+            {
+                IsRegistered = true,
+                RegChecked = true,
+                TrialVersion = false,
+                IsValid = true,
+                RegError = false
+            };
+        }
+
+        /// <summary>
+        /// Gets registration status for a feature.
+        /// </summary>
+        /// <param name="name">Feature name.</param>
+        /// <response code="501">Not implemented.</response>
+        /// <returns>Not Implemented.</returns>
+        /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
+        [Obsolete("Paid plugins are not supported")]
+        [HttpGet("/Registrations/{name}")]
+        public ActionResult GetRegistration([FromRoute] string name)
+        {
+            // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
+            // delete all these registration endpoints. They are only kept for compatibility.
+            throw new NotImplementedException();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
new file mode 100644
index 0000000000..aaaf54267a
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
@@ -0,0 +1,42 @@
+#nullable enable
+
+using System;
+
+namespace Jellyfin.Api.Models.PluginDtos
+{
+    /// <summary>
+    /// MB Registration Record.
+    /// </summary>
+    public class MBRegistrationRecord
+    {
+        /// <summary>
+        /// Gets or sets expiration date.
+        /// </summary>
+        public DateTime ExpirationDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether is registered.
+        /// </summary>
+        public bool IsRegistered { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether reg checked.
+        /// </summary>
+        public bool RegChecked { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether reg error.
+        /// </summary>
+        public bool RegError { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether trial version.
+        /// </summary>
+        public bool TrialVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether is valid.
+        /// </summary>
+        public bool IsValid { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
new file mode 100644
index 0000000000..793002a6cd
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
@@ -0,0 +1,20 @@
+#nullable enable
+
+namespace Jellyfin.Api.Models.PluginDtos
+{
+    /// <summary>
+    /// Plugin security info.
+    /// </summary>
+    public class PluginSecurityInfo
+    {
+        /// <summary>
+        /// Gets or sets the supporter key.
+        /// </summary>
+        public string? SupporterKey { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether is mb supporter.
+        /// </summary>
+        public bool IsMbSupporter { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs
deleted file mode 100644
index 7f74511eec..0000000000
--- a/MediaBrowser.Api/PluginService.cs
+++ /dev/null
@@ -1,268 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class Plugins
-    /// </summary>
-    [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")]
-    [Authenticated]
-    public class GetPlugins : IReturn<PluginInfo[]>
-    {
-        public bool? IsAppStoreEnabled { get; set; }
-    }
-
-    /// <summary>
-    /// Class UninstallPlugin
-    /// </summary>
-    [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")]
-    [Authenticated(Roles = "Admin")]
-    public class UninstallPlugin : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPluginConfiguration
-    /// </summary>
-    [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")]
-    [Authenticated]
-    public class GetPluginConfiguration
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdatePluginConfiguration
-    /// </summary>
-    [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")]
-    [Authenticated]
-    public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The raw Http Request Input Stream
-        /// </summary>
-        /// <value>The request stream.</value>
-        public Stream RequestStream { get; set; }
-    }
-
-    //TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
-    // delete all these registration endpoints. They are only kept for compatibility.
-    [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
-    [Authenticated]
-    public class GetRegistration : IReturn<RegistrationInfo>
-    {
-        [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPluginSecurityInfo
-    /// </summary>
-    [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)]
-    [Authenticated]
-    public class GetPluginSecurityInfo : IReturn<PluginSecurityInfo>
-    {
-    }
-
-    /// <summary>
-    /// Class UpdatePluginSecurityInfo
-    /// </summary>
-    [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)]
-    [Authenticated(Roles = "Admin")]
-    public class UpdatePluginSecurityInfo : PluginSecurityInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Plugins/RegistrationRecords/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
-    [Authenticated]
-    public class GetRegistrationStatus
-    {
-        [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    // TODO these two classes are only kept for compability with paid plugins and should be removed
-    public class RegistrationInfo
-    {
-        public string Name { get; set; }
-        public DateTime ExpirationDate { get; set; }
-        public bool IsTrial { get; set; }
-        public bool IsRegistered { get; set; }
-    }
-
-    public class MBRegistrationRecord
-    {
-        public DateTime ExpirationDate { get; set; }
-        public bool IsRegistered { get; set; }
-        public bool RegChecked { get; set; }
-        public bool RegError { get; set; }
-        public bool TrialVersion { get; set; }
-        public bool IsValid { get; set; }
-    }
-
-    public class PluginSecurityInfo
-    {
-        public string SupporterKey { get; set; }
-        public bool IsMBSupporter { get; set; }
-    }
-    /// <summary>
-    /// Class PluginsService
-    /// </summary>
-    public class PluginService : BaseApiService
-    {
-        /// <summary>
-        /// The _json serializer
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// The _app host
-        /// </summary>
-        private readonly IApplicationHost _appHost;
-        private readonly IInstallationManager _installationManager;
-
-        public PluginService(
-            ILogger<PluginService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IApplicationHost appHost,
-            IInstallationManager installationManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appHost = appHost;
-            _installationManager = installationManager;
-            _jsonSerializer = jsonSerializer;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRegistrationStatus request)
-        {
-            var record = new MBRegistrationRecord
-            {
-                IsRegistered = true,
-                RegChecked = true,
-                TrialVersion = false,
-                IsValid = true,
-                RegError = false
-            };
-
-            return ToOptimizedResult(record);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPlugins request)
-        {
-            var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToArray();
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPluginConfiguration request)
-        {
-            var guid = new Guid(request.Id);
-            var plugin = _appHost.Plugins.First(p => p.Id == guid) as IHasPluginConfiguration;
-
-            return ToOptimizedResult(plugin.Configuration);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPluginSecurityInfo request)
-        {
-            var result = new PluginSecurityInfo
-            {
-                IsMBSupporter = true,
-                SupporterKey = "IAmTotallyLegit"
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(UpdatePluginSecurityInfo request)
-        {
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(UpdatePluginConfiguration request)
-        {
-            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
-            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var id = Guid.Parse(GetPathValue(1));
-
-            if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin))
-            {
-                throw new FileNotFoundException();
-            }
-
-            var configuration = (await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, plugin.ConfigurationType).ConfigureAwait(false)) as BasePluginConfiguration;
-
-            plugin.UpdateConfiguration(configuration);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(UninstallPlugin request)
-        {
-            var guid = new Guid(request.Id);
-            var plugin = _appHost.Plugins.First(p => p.Id == guid);
-
-            _installationManager.UninstallPlugin(plugin);
-        }
-    }
-}

From dc190e56833235c1b20fa76b8382da80fc62b0b7 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 9 Jun 2020 18:56:17 +0200
Subject: [PATCH 163/463] Move ActivityLogService to Jellyfin.Api

---
 .../System/ActivityLogController.cs           | 54 ++++++++++++++++
 MediaBrowser.Api/System/ActivityLogService.cs | 61 -------------------
 2 files changed, 54 insertions(+), 61 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/System/ActivityLogController.cs
 delete mode 100644 MediaBrowser.Api/System/ActivityLogService.cs

diff --git a/Jellyfin.Api/Controllers/System/ActivityLogController.cs b/Jellyfin.Api/Controllers/System/ActivityLogController.cs
new file mode 100644
index 0000000000..f1daed2edd
--- /dev/null
+++ b/Jellyfin.Api/Controllers/System/ActivityLogController.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Globalization;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers.System
+{
+    /// <summary>
+    /// Activity log controller.
+    /// </summary>
+    [Route("/System/ActivityLog/Entries")]
+    [Authorize(Policy = Policies.RequiresElevation)]
+    public class ActivityLogController : BaseJellyfinApiController
+    {
+        private readonly IActivityManager _activityManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
+        /// </summary>
+        /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
+        public ActivityLogController(IActivityManager activityManager)
+        {
+            _activityManager = activityManager;
+        }
+
+        /// <summary>
+        /// Gets activity log entries.
+        /// </summary>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
+        /// <param name="hasUserId">Optional. Only returns activities that have a user associated.</param>
+        /// <response code="200">Activity log returned.</response>
+        /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string minDate,
+            bool? hasUserId)
+        {
+            DateTime? startDate = string.IsNullOrWhiteSpace(minDate) ?
+                (DateTime?)null :
+                DateTime.Parse(minDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
+
+            return _activityManager.GetActivityLogEntries(startDate, hasUserId, startIndex, limit);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
deleted file mode 100644
index f95fa7ca0b..0000000000
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using System.Globalization;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
-    [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
-    public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
-    {
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDate { get; set; }
-
-        public bool? HasUserId { get; set; }
-    }
-
-    [Authenticated(Roles = "Admin")]
-    public class ActivityLogService : BaseApiService
-    {
-        private readonly IActivityManager _activityManager;
-
-        public ActivityLogService(
-            ILogger<ActivityLogService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IActivityManager activityManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _activityManager = activityManager;
-        }
-
-        public object Get(GetActivityLogs request)
-        {
-            DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
-                (DateTime?)null :
-                DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
-            var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit);
-
-            return ToOptimizedResult(result);
-        }
-    }
-}

From 1bf6c085eda59034687f24fa5b5997389aede9e5 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 10 Jun 2020 13:09:23 +0200
Subject: [PATCH 164/463] Move File; Move Route; use DateTime? in Query

---
 .../{System => }/ActivityLogController.cs          | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)
 rename Jellyfin.Api/Controllers/{System => }/ActivityLogController.cs (80%)

diff --git a/Jellyfin.Api/Controllers/System/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
similarity index 80%
rename from Jellyfin.Api/Controllers/System/ActivityLogController.cs
rename to Jellyfin.Api/Controllers/ActivityLogController.cs
index f1daed2edd..8d37a83738 100644
--- a/Jellyfin.Api/Controllers/System/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -7,12 +7,12 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
-namespace Jellyfin.Api.Controllers.System
+namespace Jellyfin.Api.Controllers
 {
     /// <summary>
     /// Activity log controller.
     /// </summary>
-    [Route("/System/ActivityLog/Entries")]
+    [Route("/System/ActivityLog")]
     [Authorize(Policy = Policies.RequiresElevation)]
     public class ActivityLogController : BaseJellyfinApiController
     {
@@ -36,19 +36,15 @@ namespace Jellyfin.Api.Controllers.System
         /// <param name="hasUserId">Optional. Only returns activities that have a user associated.</param>
         /// <response code="200">Activity log returned.</response>
         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
-        [HttpGet]
+        [HttpGet("Entries")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string minDate,
+            [FromQuery] DateTime? minDate,
             bool? hasUserId)
         {
-            DateTime? startDate = string.IsNullOrWhiteSpace(minDate) ?
-                (DateTime?)null :
-                DateTime.Parse(minDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
-            return _activityManager.GetActivityLogEntries(startDate, hasUserId, startIndex, limit);
+            return _activityManager.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
         }
     }
 }

From b16da095493bc207f4196b8b61cfc768a237a5bc Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 10 Jun 2020 15:18:13 +0200
Subject: [PATCH 165/463] Move /System Endpoint to Jellyfin.Api

---
 Jellyfin.Api/Controllers/SystemController.cs | 222 ++++++++++++++++++
 MediaBrowser.Api/System/SystemService.cs     | 226 -------------------
 2 files changed, 222 insertions(+), 226 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SystemController.cs
 delete mode 100644 MediaBrowser.Api/System/SystemService.cs

diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
new file mode 100644
index 0000000000..cab6f308f0
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The system controller.
+    /// </summary>
+    [Route("/System")]
+    public class SystemController : BaseJellyfinApiController
+    {
+        private readonly IServerApplicationHost _appHost;
+        private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+        private readonly INetworkManager _network;
+        private readonly ILogger<SystemController> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SystemController"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+        /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
+        /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+        public SystemController(
+            IServerConfigurationManager serverConfigurationManager,
+            IServerApplicationHost appHost,
+            IFileSystem fileSystem,
+            INetworkManager network,
+            ILogger<SystemController> logger)
+        {
+            _appPaths = serverConfigurationManager.ApplicationPaths;
+            _appHost = appHost;
+            _fileSystem = fileSystem;
+            _network = network;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Gets information about the server.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
+        [HttpGet("Info")]
+        // TODO: Authorize EscapeParentalControl
+        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<SystemInfo>> GetSystemInfo()
+        {
+            return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets public information about the server.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
+        [HttpGet("Info/Public")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo()
+        {
+            return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Pings the system.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>The server name.</returns>
+        [HttpGet("Ping")]
+        [HttpPost("Ping")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<string> PingSystem()
+        {
+            return _appHost.Name;
+        }
+
+        /// <summary>
+        /// Restarts the application.
+        /// </summary>
+        /// <response code="204">Server restarted.</response>
+        /// <returns>No content. Server restarted.</returns>
+        [HttpPost("Restart")]
+        // TODO: Authorize AllowLocal = true
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult RestartApplication()
+        {
+            Task.Run(async () =>
+            {
+                await Task.Delay(100).ConfigureAwait(false);
+                _appHost.Restart();
+            });
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Shuts down the application.
+        /// </summary>
+        /// <response code="204">Server shut down.</response>
+        /// <returns>No content. Server shut down.</returns>
+        [HttpPost("Shutdown")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult ShutdownApplication()
+        {
+            Task.Run(async () =>
+            {
+                await Task.Delay(100).ConfigureAwait(false);
+                await _appHost.Shutdown().ConfigureAwait(false);
+            });
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets a list of available server log files.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns>
+        [HttpGet("Logs")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<LogFile[]> GetServerLogs()
+        {
+            IEnumerable<FileSystemMetadata> files;
+
+            try
+            {
+                files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
+            }
+            catch (IOException ex)
+            {
+                _logger.LogError(ex, "Error getting logs");
+                files = Enumerable.Empty<FileSystemMetadata>();
+            }
+
+            var result = files.Select(i => new LogFile
+                {
+                    DateCreated = _fileSystem.GetCreationTimeUtc(i),
+                    DateModified = _fileSystem.GetLastWriteTimeUtc(i),
+                    Name = i.Name,
+                    Size = i.Length
+                })
+                .OrderByDescending(i => i.DateModified)
+                .ThenByDescending(i => i.DateCreated)
+                .ThenBy(i => i.Name)
+                .ToArray();
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets information about the request endpoint.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
+        [HttpGet("Endpoint")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<EndPointInfo> GetEndpointInfo()
+        {
+            return new EndPointInfo
+            {
+                IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress),
+                IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString())
+            };
+        }
+
+        /// <summary>
+        /// Gets a log file.
+        /// </summary>
+        /// <param name="name">The name of the log file to get.</param>
+        /// <response code="200">Log file retrieved.</response>
+        /// <returns>The log file.</returns>
+        [HttpGet("Logs/Log")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetLogFile([FromQuery, Required] string name)
+        {
+            var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
+                .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+
+            // For older files, assume fully static
+            var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
+
+            FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+            return File(stream, "text/plain");
+        }
+
+        /// <summary>
+        /// Gets wake on lan information.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
+        [HttpGet("WakeOnLanInfo")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
+        {
+            var result = _appHost.GetWakeOnLanInfo();
+            return Ok(result);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs
deleted file mode 100644
index c57cc93d55..0000000000
--- a/MediaBrowser.Api/System/SystemService.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
-    /// <summary>
-    /// Class GetSystemInfo
-    /// </summary>
-    [Route("/System/Info", "GET", Summary = "Gets information about the server")]
-    [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)]
-    public class GetSystemInfo : IReturn<SystemInfo>
-    {
-
-    }
-
-    [Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")]
-    public class GetPublicSystemInfo : IReturn<PublicSystemInfo>
-    {
-
-    }
-
-    [Route("/System/Ping", "POST")]
-    [Route("/System/Ping", "GET")]
-    public class PingSystem : IReturnVoid
-    {
-
-    }
-
-    /// <summary>
-    /// Class RestartApplication
-    /// </summary>
-    [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
-    [Authenticated(Roles = "Admin", AllowLocal = true)]
-    public class RestartApplication
-    {
-    }
-
-    /// <summary>
-    /// This is currently not authenticated because the uninstaller needs to be able to shutdown the server.
-    /// </summary>
-    [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
-    [Authenticated(Roles = "Admin", AllowLocal = true)]
-    public class ShutdownApplication
-    {
-    }
-
-    [Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")]
-    [Authenticated(Roles = "Admin")]
-    public class GetServerLogs : IReturn<LogFile[]>
-    {
-    }
-
-    [Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")]
-    [Authenticated]
-    public class GetEndpointInfo : IReturn<EndPointInfo>
-    {
-        public string Endpoint { get; set; }
-    }
-
-    [Route("/System/Logs/Log", "GET", Summary = "Gets a log file")]
-    [Authenticated(Roles = "Admin")]
-    public class GetLogFile
-    {
-        [ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Name { get; set; }
-    }
-
-    [Route("/System/WakeOnLanInfo", "GET", Summary = "Gets wake on lan information")]
-    [Authenticated]
-    public class GetWakeOnLanInfo : IReturn<WakeOnLanInfo[]>
-    {
-
-    }
-
-    /// <summary>
-    /// Class SystemInfoService
-    /// </summary>
-    public class SystemService : BaseApiService
-    {
-        /// <summary>
-        /// The _app host
-        /// </summary>
-        private readonly IServerApplicationHost _appHost;
-        private readonly IApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-
-        private readonly INetworkManager _network;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SystemService" /> class.
-        /// </summary>
-        /// <param name="appHost">The app host.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <exception cref="ArgumentNullException">jsonSerializer</exception>
-        public SystemService(
-            ILogger<SystemService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IServerApplicationHost appHost,
-            IFileSystem fileSystem,
-            INetworkManager network)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _appHost = appHost;
-            _fileSystem = fileSystem;
-            _network = network;
-        }
-
-        public object Post(PingSystem request)
-        {
-            return _appHost.Name;
-        }
-
-        public object Get(GetWakeOnLanInfo request)
-        {
-            var result = _appHost.GetWakeOnLanInfo();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetServerLogs request)
-        {
-            IEnumerable<FileSystemMetadata> files;
-
-            try
-            {
-                files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
-            }
-            catch (IOException ex)
-            {
-                Logger.LogError(ex, "Error getting logs");
-                files = Enumerable.Empty<FileSystemMetadata>();
-            }
-
-            var result = files.Select(i => new LogFile
-            {
-                DateCreated = _fileSystem.GetCreationTimeUtc(i),
-                DateModified = _fileSystem.GetLastWriteTimeUtc(i),
-                Name = i.Name,
-                Size = i.Length
-
-            }).OrderByDescending(i => i.DateModified)
-                .ThenByDescending(i => i.DateCreated)
-                .ThenBy(i => i.Name)
-                .ToArray();
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task<object> Get(GetLogFile request)
-        {
-            var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
-                .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
-
-            // For older files, assume fully static
-            var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
-
-            return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetSystemInfo request)
-        {
-            var result = await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetPublicSystemInfo request)
-        {
-            var result = await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RestartApplication request)
-        {
-            _appHost.Restart();
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(ShutdownApplication request)
-        {
-            Task.Run(async () =>
-            {
-                await Task.Delay(100).ConfigureAwait(false);
-                await _appHost.Shutdown().ConfigureAwait(false);
-            });
-        }
-
-        public object Get(GetEndpointInfo request)
-        {
-            return ToOptimizedResult(new EndPointInfo
-            {
-                IsLocal = Request.IsLocal,
-                IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp)
-            });
-        }
-    }
-}

From 6a70081643de80a2053b5903644cdcfa4bcbfc61 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 10 Jun 2020 15:57:31 +0200
Subject: [PATCH 166/463] Move ApiKeyService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/ApiKeyController.cs | 97 ++++++++++++++++++++
 MediaBrowser.Api/Sessions/ApiKeyService.cs   | 85 -----------------
 2 files changed, 97 insertions(+), 85 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ApiKeyController.cs
 delete mode 100644 MediaBrowser.Api/Sessions/ApiKeyService.cs

diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
new file mode 100644
index 0000000000..ed521c1fc5
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -0,0 +1,97 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Authentication controller.
+    /// </summary>
+    [Route("/Auth")]
+    public class ApiKeyController : BaseJellyfinApiController
+    {
+        private readonly ISessionManager _sessionManager;
+        private readonly IServerApplicationHost _appHost;
+        private readonly IAuthenticationRepository _authRepo;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
+        /// </summary>
+        /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
+        public ApiKeyController(
+            ISessionManager sessionManager,
+            IServerApplicationHost appHost,
+            IAuthenticationRepository authRepo)
+        {
+            _sessionManager = sessionManager;
+            _appHost = appHost;
+            _authRepo = authRepo;
+        }
+
+        /// <summary>
+        /// Get all keys.
+        /// </summary>
+        /// <response code="200">Api keys retrieved.</response>
+        /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
+        [HttpGet("Keys")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
+        {
+            var result = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                HasUser = false
+            });
+
+            return result;
+        }
+
+        /// <summary>
+        /// Create a new api key.
+        /// </summary>
+        /// <param name="app">Name of the app using the authentication key.</param>
+        /// <response code="204">Api key created.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("Keys")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult CreateKey([FromQuery, Required] string app)
+        {
+            _authRepo.Create(new AuthenticationInfo
+            {
+                AppName = app,
+                AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
+                DateCreated = DateTime.UtcNow,
+                DeviceId = _appHost.SystemId,
+                DeviceName = _appHost.FriendlyName,
+                AppVersion = _appHost.ApplicationVersionString
+            });
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Remove an api key.
+        /// </summary>
+        /// <param name="key">The access token to delete.</param>
+        /// <response code="204">Api key deleted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("Keys/{key}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult RevokeKey([FromRoute] string key)
+        {
+            _sessionManager.RevokeToken(key);
+            return NoContent();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Sessions/ApiKeyService.cs b/MediaBrowser.Api/Sessions/ApiKeyService.cs
deleted file mode 100644
index 5102ce0a7c..0000000000
--- a/MediaBrowser.Api/Sessions/ApiKeyService.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Globalization;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
-    [Route("/Auth/Keys", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetKeys
-    {
-    }
-
-    [Route("/Auth/Keys/{Key}", "DELETE")]
-    [Authenticated(Roles = "Admin")]
-    public class RevokeKey
-    {
-        [ApiMember(Name = "Key", Description = "Authentication key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Key { get; set; }
-    }
-
-    [Route("/Auth/Keys", "POST")]
-    [Authenticated(Roles = "Admin")]
-    public class CreateKey
-    {
-        [ApiMember(Name = "App", Description = "Name of the app using the authentication key", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string App { get; set; }
-    }
-
-    public class ApiKeyService : BaseApiService
-    {
-        private readonly ISessionManager _sessionManager;
-
-        private readonly IAuthenticationRepository _authRepo;
-
-        private readonly IServerApplicationHost _appHost;
-
-        public ApiKeyService(
-            ILogger<ApiKeyService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISessionManager sessionManager,
-            IServerApplicationHost appHost,
-            IAuthenticationRepository authRepo)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _sessionManager = sessionManager;
-            _authRepo = authRepo;
-            _appHost = appHost;
-        }
-
-        public void Delete(RevokeKey request)
-        {
-            _sessionManager.RevokeToken(request.Key);
-        }
-
-        public void Post(CreateKey request)
-        {
-            _authRepo.Create(new AuthenticationInfo
-            {
-                AppName = request.App,
-                AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
-                DateCreated = DateTime.UtcNow,
-                DeviceId = _appHost.SystemId,
-                DeviceName = _appHost.FriendlyName,
-                AppVersion = _appHost.ApplicationVersionString
-            });
-        }
-
-        public object Get(GetKeys request)
-        {
-            var result = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                HasUser = false
-            });
-
-            return result;
-        }
-    }
-}

From 393f5f0c2581a19abf4edf500802f9556117ce7a Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Wed, 10 Jun 2020 09:23:20 -0600
Subject: [PATCH 167/463] Update Jellyfin.Api/Controllers/FilterController.cs

Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com>
---
 Jellyfin.Api/Controllers/FilterController.cs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d06c5e96c9..0f6124714f 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -195,10 +195,10 @@ namespace Jellyfin.Api.Controllers
                 genreQuery.Parent = parentItem;
             }
 
-            if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
             {
                 filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
                 {
@@ -218,4 +218,4 @@ namespace Jellyfin.Api.Controllers
             return filters;
         }
     }
-}
\ No newline at end of file
+}

From 355682620d120ded27c33b528e554982946de86c Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Wed, 10 Jun 2020 09:23:27 -0600
Subject: [PATCH 168/463] Update Jellyfin.Api/Controllers/FilterController.cs

Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com>
---
 Jellyfin.Api/Controllers/FilterController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 0f6124714f..431114ea9a 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -62,10 +62,10 @@ namespace Jellyfin.Api.Controllers
                 ? null
                 : _userManager.GetUserById(userId.Value);
 
-            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
             {
                 parentItem = null;
             }

From f64e8e8757c01d45779f90a38e1bb0033c197353 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 10 Jun 2020 18:20:54 +0200
Subject: [PATCH 169/463] Move SubtitleService to Jellyfin.Api

---
 .../Controllers/SubtitleController.cs         | 344 ++++++++++++++++++
 MediaBrowser.Api/Subtitles/SubtitleService.cs | 300 ---------------
 2 files changed, 344 insertions(+), 300 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SubtitleController.cs
 delete mode 100644 MediaBrowser.Api/Subtitles/SubtitleService.cs

diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
new file mode 100644
index 0000000000..fe5e133386
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -0,0 +1,344 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Subtitle controller.
+    /// </summary>
+    public class SubtitleController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly ISubtitleManager _subtitleManager;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ILogger<SubtitleController> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SubtitleController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+        /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
+        public SubtitleController(
+            ILibraryManager libraryManager,
+            ISubtitleManager subtitleManager,
+            ISubtitleEncoder subtitleEncoder,
+            IMediaSourceManager mediaSourceManager,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            IAuthorizationContext authContext,
+            ILogger<SubtitleController> logger)
+        {
+            _libraryManager = libraryManager;
+            _subtitleManager = subtitleManager;
+            _subtitleEncoder = subtitleEncoder;
+            _mediaSourceManager = mediaSourceManager;
+            _providerManager = providerManager;
+            _fileSystem = fileSystem;
+            _authContext = authContext;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Deletes an external subtitle file.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="index">The index of the subtitle file.</param>
+        /// <response code="204">Subtitle deleted.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Videos/{id}/Subtitles/{index}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<Task> DeleteSubtitle(
+            [FromRoute] Guid id,
+            [FromRoute] int index)
+        {
+            var item = _libraryManager.GetItemById(id);
+
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            _subtitleManager.DeleteSubtitles(item, index);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Search remote subtitles.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="language">The language of the subtitles.</param>
+        /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
+        /// <response code="200">Subtitles retrieved.</response>
+        /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
+        [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<RemoteSubtitleInfo[]>> SearchRemoteSubtitles(
+            [FromRoute] Guid id,
+            [FromRoute] string language,
+            [FromQuery] bool isPerfectMatch)
+        {
+            var video = (Video)_libraryManager.GetItemById(id);
+
+            return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Downloads a remote subtitle.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="subtitleId">The subtitle id.</param>
+        /// <response code="204">Subtitle downloaded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult DownloadRemoteSubtitles(
+            [FromRoute] Guid id,
+            [FromRoute] string subtitleId)
+        {
+            var video = (Video)_libraryManager.GetItemById(id);
+
+            Task.Run(async () =>
+            {
+                try
+                {
+                    await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+                        .ConfigureAwait(false);
+
+                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error downloading subtitles");
+                }
+            });
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets the remote subtitles.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <response code="200">File returned.</response>
+        /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
+        [HttpGet("/Providers/Subtitles/Subtitles/{id}")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
+        {
+            var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
+
+            return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+        }
+
+        /// <summary>
+        /// Gets subtitles in a specified format.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="mediaSourceId">The media source id.</param>
+        /// <param name="index">The subtitle stream index.</param>
+        /// <param name="format">The format of the returned subtitle.</param>
+        /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
+        /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
+        /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
+        /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
+        /// <response code="200">File returned.</response>
+        /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
+        [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}")]
+        [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetSubtitle(
+            [FromRoute, Required] Guid id,
+            [FromRoute, Required] string mediaSourceId,
+            [FromRoute, Required] int index,
+            [FromRoute, Required] string format,
+            [FromRoute] long startPositionTicks,
+            [FromQuery] long? endPositionTicks,
+            [FromQuery] bool copyTimestamps,
+            [FromQuery] bool addVttTimeMap)
+        {
+            if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
+            {
+                format = "json";
+            }
+
+            if (string.IsNullOrEmpty(format))
+            {
+                var item = (Video)_libraryManager.GetItemById(id);
+
+                var idString = id.ToString("N", CultureInfo.InvariantCulture);
+                var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
+                    .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
+
+                var subtitleStream = mediaSource.MediaStreams
+                    .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
+
+                FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
+                return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
+            }
+
+            if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
+            {
+                using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+                using var reader = new StreamReader(stream);
+
+                var text = reader.ReadToEnd();
+
+                text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
+
+                return File(text, MimeTypes.GetMimeType("file." + format));
+            }
+
+            return File(
+                await EncodeSubtitles(
+                    id,
+                    mediaSourceId,
+                    index,
+                    format,
+                    startPositionTicks,
+                    endPositionTicks,
+                    copyTimestamps).ConfigureAwait(false),
+                MimeTypes.GetMimeType("file." + format));
+        }
+
+        /// <summary>
+        /// Gets an HLS subtitle playlist.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="index">The subtitle stream index.</param>
+        /// <param name="mediaSourceId">The media source id.</param>
+        /// <param name="segmentLength">The subtitle segment length.</param>
+        /// <response code="200">Subtitle playlist retrieved.</response>
+        /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
+        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetSubtitlePlaylist(
+            [FromRoute] Guid id,
+            [FromRoute] int index,
+            [FromRoute] string mediaSourceId,
+            [FromQuery, Required] int segmentLength)
+        {
+            var item = (Video)_libraryManager.GetItemById(id);
+
+            var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
+
+            var builder = new StringBuilder();
+
+            var runtime = mediaSource.RunTimeTicks ?? -1;
+
+            if (runtime <= 0)
+            {
+                throw new ArgumentException("HLS Subtitles are not supported for this media.");
+            }
+
+            var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
+            if (segmentLengthTicks <= 0)
+            {
+                throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
+            }
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+
+            long positionTicks = 0;
+
+            var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
+
+            while (positionTicks < runtime)
+            {
+                var remaining = runtime - positionTicks;
+                var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+
+                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
+
+                var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+
+                var url = string.Format(
+                    CultureInfo.CurrentCulture,
+                    "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+                    positionTicks.ToString(CultureInfo.InvariantCulture),
+                    endPositionTicks.ToString(CultureInfo.InvariantCulture),
+                    accessToken);
+
+                builder.AppendLine(url);
+
+                positionTicks += segmentLengthTicks;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+            return File(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"));
+        }
+
+        /// <summary>
+        /// Encodes a subtitle in the specified format.
+        /// </summary>
+        /// <param name="id">The media id.</param>
+        /// <param name="mediaSourceId">The source media id.</param>
+        /// <param name="index">The subtitle index.</param>
+        /// <param name="format">The format to convert to.</param>
+        /// <param name="startPositionTicks">The start position in ticks.</param>
+        /// <param name="endPositionTicks">The end position in ticks.</param>
+        /// <param name="copyTimestamps">Whether to copy the timestamps.</param>
+        /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
+        private Task<Stream> EncodeSubtitles(
+            Guid id,
+            string mediaSourceId,
+            int index,
+            string format,
+            long startPositionTicks,
+            long? endPositionTicks,
+            bool copyTimestamps)
+        {
+            var item = _libraryManager.GetItemById(id);
+
+            return _subtitleEncoder.GetSubtitles(
+                item,
+                mediaSourceId,
+                index,
+                format,
+                startPositionTicks,
+                endPositionTicks ?? 0,
+                copyTimestamps,
+                CancellationToken.None);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs
deleted file mode 100644
index f2968c6b5c..0000000000
--- a/MediaBrowser.Api/Subtitles/SubtitleService.cs
+++ /dev/null
@@ -1,300 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Subtitles
-{
-    [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
-    [Authenticated(Roles = "Admin")]
-    public class DeleteSubtitle
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
-        public int Index { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
-    [Authenticated]
-    public class SearchRemoteSubtitles : IReturn<RemoteSubtitleInfo[]>
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Language { get; set; }
-
-        public bool? IsPerfectMatch { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
-    [Authenticated]
-    public class DownloadRemoteSubtitles : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SubtitleId { get; set; }
-    }
-
-    [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
-    [Authenticated]
-    public class GetRemoteSubtitles : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
-    public class GetSubtitle
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-
-        [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Format { get; set; }
-
-        [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public long StartPositionTicks { get; set; }
-
-        [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public long? EndPositionTicks { get; set; }
-
-        [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool CopyTimestamps { get; set; }
-        public bool AddVttTimeMap { get; set; }
-    }
-
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
-    [Authenticated]
-    public class GetSubtitlePlaylist
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-
-        [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int SegmentLength { get; set; }
-    }
-
-    public class SubtitleService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ISubtitleManager _subtitleManager;
-        private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IAuthorizationContext _authContext;
-
-        public SubtitleService(
-            ILogger<SubtitleService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            ISubtitleManager subtitleManager,
-            ISubtitleEncoder subtitleEncoder,
-            IMediaSourceManager mediaSourceManager,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _subtitleManager = subtitleManager;
-            _subtitleEncoder = subtitleEncoder;
-            _mediaSourceManager = mediaSourceManager;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
-            _authContext = authContext;
-        }
-
-        public async Task<object> Get(GetSubtitlePlaylist request)
-        {
-            var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
-
-            var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
-
-            var builder = new StringBuilder();
-
-            var runtime = mediaSource.RunTimeTicks ?? -1;
-
-            if (runtime <= 0)
-            {
-                throw new ArgumentException("HLS Subtitles are not supported for this media.");
-            }
-
-            var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
-            if (segmentLengthTicks <= 0)
-            {
-                throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
-            }
-
-            builder.AppendLine("#EXTM3U");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
-            builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
-
-            long positionTicks = 0;
-
-            var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
-
-            while (positionTicks < runtime)
-            {
-                var remaining = runtime - positionTicks;
-                var lengthTicks = Math.Min(remaining, segmentLengthTicks);
-
-                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
-
-                var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
-
-                var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
-                    positionTicks.ToString(CultureInfo.InvariantCulture),
-                    endPositionTicks.ToString(CultureInfo.InvariantCulture),
-                    accessToken);
-
-                builder.AppendLine(url);
-
-                positionTicks += segmentLengthTicks;
-            }
-
-            builder.AppendLine("#EXT-X-ENDLIST");
-
-            return ResultFactory.GetResult(Request, builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-        }
-
-        public async Task<object> Get(GetSubtitle request)
-        {
-            if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
-            {
-                request.Format = "json";
-            }
-            if (string.IsNullOrEmpty(request.Format))
-            {
-                var item = (Video)_libraryManager.GetItemById(request.Id);
-
-                var idString = request.Id.ToString("N", CultureInfo.InvariantCulture);
-                var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null)
-                    .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString));
-
-                var subtitleStream = mediaSource.MediaStreams
-                    .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index);
-
-                return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false);
-            }
-
-            if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap)
-            {
-                using var stream = await GetSubtitles(request).ConfigureAwait(false);
-                using var reader = new StreamReader(stream);
-
-                var text = reader.ReadToEnd();
-
-                text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
-
-                return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format));
-            }
-
-            return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format));
-        }
-
-        private Task<Stream> GetSubtitles(GetSubtitle request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return _subtitleEncoder.GetSubtitles(item,
-                request.MediaSourceId,
-                request.Index,
-                request.Format,
-                request.StartPositionTicks,
-                request.EndPositionTicks ?? 0,
-                request.CopyTimestamps,
-                CancellationToken.None);
-        }
-
-        public async Task<object> Get(SearchRemoteSubtitles request)
-        {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
-
-            return await _subtitleManager.SearchSubtitles(video, request.Language, request.IsPerfectMatch, CancellationToken.None).ConfigureAwait(false);
-        }
-
-        public Task Delete(DeleteSubtitle request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-            return _subtitleManager.DeleteSubtitles(item, request.Index);
-        }
-
-        public async Task<object> Get(GetRemoteSubtitles request)
-        {
-            var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ResultFactory.GetResult(Request, result.Stream, MimeTypes.GetMimeType("file." + result.Format));
-        }
-
-        public void Post(DownloadRemoteSubtitles request)
-        {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
-
-            Task.Run(async () =>
-            {
-                try
-                {
-                    await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error downloading subtitles");
-                }
-            });
-        }
-    }
-}

From 8178b194708f4added870597500a88d0ba5a3cfa Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 11 Jun 2020 12:29:56 +0200
Subject: [PATCH 170/463] Fix suggestions

---
 .../Controllers/SubtitleController.cs         | 47 ++++++++++---------
 1 file changed, 25 insertions(+), 22 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index fe5e133386..15c7d1eaad 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -1,8 +1,12 @@
-using System;
+#nullable enable
+
+using System;
+using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net.Mime;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -107,10 +111,10 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<RemoteSubtitleInfo[]>> SearchRemoteSubtitles(
+        public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid id,
             [FromRoute] string language,
-            [FromQuery] bool isPerfectMatch)
+            [FromQuery] bool? isPerfectMatch)
         {
             var video = (Video)_libraryManager.GetItemById(id);
 
@@ -127,26 +131,24 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult DownloadRemoteSubtitles(
+        public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid id,
             [FromRoute] string subtitleId)
         {
             var video = (Video)_libraryManager.GetItemById(id);
 
-            Task.Run(async () =>
+            try
+            {
+                await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+                    .ConfigureAwait(false);
+
+                _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+            }
+            catch (Exception ex)
             {
-                try
-                {
-                    await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error downloading subtitles");
-                }
-            });
+                _logger.LogError(ex, "Error downloading subtitles");
+            }
+
             return NoContent();
         }
 
@@ -159,6 +161,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Providers/Subtitles/Subtitles/{id}")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [Produces(MediaTypeNames.Application.Octet)]
         public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
         {
             var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
@@ -179,8 +182,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
-        [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}")]
-        [HttpGet("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}")]
+        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitle(
             [FromRoute, Required] Guid id,
@@ -217,11 +220,11 @@ namespace Jellyfin.Api.Controllers
                 using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
                 using var reader = new StreamReader(stream);
 
-                var text = reader.ReadToEnd();
+                var text = await reader.ReadToEndAsync().ConfigureAwait(false);
 
                 text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
 
-                return File(text, MimeTypes.GetMimeType("file." + format));
+                return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
             }
 
             return File(
@@ -305,7 +308,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             builder.AppendLine("#EXT-X-ENDLIST");
-            return File(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"));
+            return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
         }
 
         /// <summary>

From fcbae95d1945ad5d632c5c86253c02da657db339 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 11 Jun 2020 15:57:31 +0200
Subject: [PATCH 171/463] Use 'await using Stream' instead of 'using Stream'

---
 Jellyfin.Api/Controllers/SubtitleController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 15c7d1eaad..ba2250d81a 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers
 
             if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
             {
-                using var stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+                await using Stream stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
                 using var reader = new StreamReader(stream);
 
                 var text = await reader.ReadToEndAsync().ConfigureAwait(false);

From a47ff4043f2116716d5f15d1f79657550052bde8 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 11 Jun 2020 16:01:41 +0200
Subject: [PATCH 172/463] Disable CA1801

---
 Jellyfin.Api/Controllers/SubtitleController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index ba2250d81a..97df8c60d8 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -1,4 +1,5 @@
 #nullable enable
+#pragma warning disable CA1801
 
 using System;
 using System.Collections.Generic;
@@ -253,6 +254,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitlePlaylist(
             [FromRoute] Guid id,
+            // TODO: 'int index' is never used: CA1801 is disabled
             [FromRoute] int index,
             [FromRoute] string mediaSourceId,
             [FromQuery, Required] int segmentLength)

From 043d76bd6e9e4a2e1093ae0e5ba025de1438b528 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 12 Jun 2020 12:38:13 +0200
Subject: [PATCH 173/463] Use Http status code 204 instead of 200

---
 .../Controllers/ConfigurationController.cs    | 18 +++++------
 Jellyfin.Api/Controllers/DevicesController.cs | 16 +++++-----
 .../Controllers/NotificationsController.cs    | 24 +++++++-------
 Jellyfin.Api/Controllers/PackageController.cs | 15 +++++----
 Jellyfin.Api/Controllers/StartupController.cs | 32 +++++++++----------
 5 files changed, 53 insertions(+), 52 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 2a1dce74d4..780a38aa81 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -53,15 +53,15 @@ namespace Jellyfin.Api.Controllers
         /// Updates application configuration.
         /// </summary>
         /// <param name="configuration">Configuration.</param>
-        /// <response code="200">Configuration updated.</response>
+        /// <response code="204">Configuration updated.</response>
         /// <returns>Update status.</returns>
         [HttpPost("Configuration")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
         {
             _configurationManager.ReplaceConfiguration(configuration);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -81,17 +81,17 @@ namespace Jellyfin.Api.Controllers
         /// Updates named configuration.
         /// </summary>
         /// <param name="key">Configuration key.</param>
-        /// <response code="200">Named configuration updated.</response>
+        /// <response code="204">Named configuration updated.</response>
         /// <returns>Update status.</returns>
         [HttpPost("Configuration/{Key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
             _configurationManager.SaveConfiguration(key, configuration);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -111,15 +111,15 @@ namespace Jellyfin.Api.Controllers
         /// Updates the path to the media encoder.
         /// </summary>
         /// <param name="mediaEncoderPath">Media encoder path form body.</param>
-        /// <response code="200">Media encoder path updated.</response>
+        /// <response code="204">Media encoder path updated.</response>
         /// <returns>Status.</returns>
         [HttpPost("MediaEncoder/Path")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
         {
             _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 1e75579033..1754b0cbda 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -105,12 +105,12 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="id">Device Id.</param>
         /// <param name="deviceOptions">Device Options.</param>
-        /// <response code="200">Device options updated.</response>
+        /// <response code="204">Device options updated.</response>
         /// <response code="404">Device not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpPost("Options")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateDeviceOptions(
             [FromQuery, BindRequired] string id,
@@ -123,18 +123,18 @@ namespace Jellyfin.Api.Controllers
             }
 
             _deviceManager.UpdateDeviceOptions(id, deviceOptions);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
         /// Deletes a device.
         /// </summary>
         /// <param name="id">Device Id.</param>
-        /// <response code="200">Device deleted.</response>
+        /// <response code="204">Device deleted.</response>
         /// <response code="404">Device not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpDelete]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
         {
             var existingDevice = _deviceManager.GetDevice(id);
@@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
                 _sessionManager.Logout(session);
             }
 
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 8d82ca10f1..5af1947562 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -99,10 +99,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
-        /// <response code="200">Notification sent.</response>
-        /// <returns>An <cref see="OkResult"/>.</returns>
+        /// <response code="204">Notification sent.</response>
+        /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("Admin")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateAdminNotification(
             [FromQuery] string name,
             [FromQuery] string description,
@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
 
             _notificationManager.SendNotification(notification, CancellationToken.None);
 
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -129,15 +129,15 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
-        /// <response code="200">Notifications set as read.</response>
-        /// <returns>An <cref see="OkResult"/>.</returns>
+        /// <response code="204">Notifications set as read.</response>
+        /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{UserID}/Read")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRead(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -145,15 +145,15 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">The userID.</param>
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
-        /// <response code="200">Notifications set as unread.</response>
-        /// <returns>An <cref see="OkResult"/>.</returns>
+        /// <response code="204">Notifications set as unread.</response>
+        /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{UserID}/Unread")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetUnread(
             [FromRoute] string userId,
             [FromQuery] string ids)
         {
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index f37319c19e..8200f891c8 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -72,11 +72,11 @@ namespace Jellyfin.Api.Controllers
         /// <param name="name">Package name.</param>
         /// <param name="assemblyGuid">GUID of the associated assembly.</param>
         /// <param name="version">Optional version. Defaults to latest version.</param>
-        /// <response code="200">Package found.</response>
+        /// <response code="204">Package found.</response>
         /// <response code="404">Package not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
         [HttpPost("/Installed/{Name}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> InstallPackage(
@@ -98,23 +98,24 @@ namespace Jellyfin.Api.Controllers
 
             await _installationManager.InstallPackage(package).ConfigureAwait(false);
 
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
         /// Cancels a package installation.
         /// </summary>
         /// <param name="id">Installation Id.</param>
-        /// <response code="200">Installation cancelled.</response>
-        /// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns>
+        /// <response code="204">Installation cancelled.</response>
+        /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
         [HttpDelete("/Installing/{id}")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public IActionResult CancelPackageInstallation(
             [FromRoute] [Required] string id)
         {
             _installationManager.CancelInstallation(new Guid(id));
 
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 57a02e62a9..aae066e0e1 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -33,16 +33,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Completes the startup wizard.
         /// </summary>
-        /// <response code="200">Startup wizard completed.</response>
-        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+        /// <response code="204">Startup wizard completed.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Complete")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CompleteWizard()
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.SetOptimalValues();
             _config.SaveConfiguration();
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="uiCulture">The UI language culture.</param>
         /// <param name="metadataCountryCode">The metadata country code.</param>
         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
-        /// <response code="200">Configuration saved.</response>
-        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+        /// <response code="204">Configuration saved.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Configuration")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateInitialConfiguration(
             [FromForm] string uiCulture,
             [FromForm] string metadataCountryCode,
@@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers
             _config.Configuration.MetadataCountryCode = metadataCountryCode;
             _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
             _config.SaveConfiguration();
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="enableRemoteAccess">Enable remote access.</param>
         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
-        /// <response code="200">Configuration saved.</response>
-        /// <returns>An <see cref="OkResult"/> indicating success.</returns>
+        /// <response code="204">Configuration saved.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("RemoteAccess")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
         {
             _config.Configuration.EnableRemoteAccess = enableRemoteAccess;
             _config.Configuration.EnableUPnP = enableAutomaticPortMapping;
             _config.SaveConfiguration();
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -121,13 +121,13 @@ namespace Jellyfin.Api.Controllers
         /// Sets the user name and password.
         /// </summary>
         /// <param name="startupUserDto">The DTO containing username and password.</param>
-        /// <response code="200">Updated user name and password.</response>
+        /// <response code="204">Updated user name and password.</response>
         /// <returns>
         /// A <see cref="Task" /> that represents the asynchronous update operation.
-        /// The task result contains an <see cref="OkResult"/> indicating success.
+        /// The task result contains a <see cref="NoContentResult"/> indicating success.
         /// </returns>
         [HttpPost("User")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
         {
             var user = _userManager.Users.First();
@@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers
                 await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
             }
 
-            return Ok();
+            return NoContent();
         }
     }
 }

From 618b893c481a658820370cdf4f62c5a9a2deebd1 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 12 Jun 2020 15:39:06 +0200
Subject: [PATCH 174/463] Move LibraryStructureService to Jellyfin.Api

---
 .../Controllers/LibraryStructureController.cs | 347 +++++++++++++++
 .../Library/LibraryStructureService.cs        | 412 ------------------
 2 files changed, 347 insertions(+), 412 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/LibraryStructureController.cs
 delete mode 100644 MediaBrowser.Api/Library/LibraryStructureService.cs

diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
new file mode 100644
index 0000000000..f074a61dbe
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -0,0 +1,347 @@
+#pragma warning disable CA1801
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The library structure controller.
+    /// </summary>
+    [Route("/Library/VirtualFolders")]
+    [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+    public class LibraryStructureController : BaseJellyfinApiController
+    {
+        private readonly IServerApplicationPaths _appPaths;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILibraryMonitor _libraryMonitor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
+        public LibraryStructureController(
+            IServerConfigurationManager serverConfigurationManager,
+            ILibraryManager libraryManager,
+            ILibraryMonitor libraryMonitor)
+        {
+            _appPaths = serverConfigurationManager?.ApplicationPaths;
+            _libraryManager = libraryManager;
+            _libraryMonitor = libraryMonitor;
+        }
+
+        /// <summary>
+        /// Gets all virtual folders.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <response code="200">Virtual folders retrieved.</response>
+        /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
+        {
+            return _libraryManager.GetVirtualFolders(true);
+        }
+
+        /// <summary>
+        /// Adds a virtual folder.
+        /// </summary>
+        /// <param name="name">The name of the virtual folder.</param>
+        /// <param name="collectionType">The type of the collection.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
+        /// <param name="paths">The paths of the virtual folder.</param>
+        /// <param name="libraryOptions">The library options.</param>
+        /// <response code="204">Folder added.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult AddVirtualFolder(
+            [FromQuery] string name,
+            [FromQuery] string collectionType,
+            [FromQuery] bool refreshLibrary,
+            [FromQuery] string[] paths,
+            [FromQuery] LibraryOptions libraryOptions)
+        {
+            libraryOptions ??= new LibraryOptions();
+
+            if (paths != null && paths.Length > 0)
+            {
+                libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
+            }
+
+            _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Removes a virtual folder.
+        /// </summary>
+        /// <param name="name">The name of the folder.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
+        /// <response code="204">Folder removed.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult RemoveVirtualFolder(
+            [FromQuery] string name,
+            [FromQuery] bool refreshLibrary)
+        {
+            _libraryManager.RemoveVirtualFolder(name, refreshLibrary);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Renames a virtual folder.
+        /// </summary>
+        /// <param name="name">The name of the virtual folder.</param>
+        /// <param name="newName">The new name.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
+        /// <response code="204">Folder renamed.</response>
+        /// <response code="404">Library doesn't exist.</response>
+        /// <response code="409">Library already exists.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
+        /// <exception cref="ArgumentNullException">The new name may not be null.</exception>
+        [HttpPost("Name")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesResponseType(StatusCodes.Status409Conflict)]
+        public ActionResult RenameVirtualFolder(
+            [FromQuery] string name,
+            [FromQuery] string newName,
+            [FromQuery] bool refreshLibrary)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            if (string.IsNullOrWhiteSpace(newName))
+            {
+                throw new ArgumentNullException(nameof(newName));
+            }
+
+            var rootFolderPath = _appPaths.DefaultUserViewsPath;
+
+            var currentPath = Path.Combine(rootFolderPath, name);
+            var newPath = Path.Combine(rootFolderPath, newName);
+
+            if (!Directory.Exists(currentPath))
+            {
+                return NotFound("The media collection does not exist.");
+            }
+
+            if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+            {
+                return Conflict($"The media library already exists at {newPath}.");
+            }
+
+            _libraryMonitor.Stop();
+
+            try
+            {
+                // Changing capitalization. Handle windows case insensitivity
+                if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
+                {
+                    var tempPath = Path.Combine(
+                        rootFolderPath,
+                        Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
+                    Directory.Move(currentPath, tempPath);
+                    currentPath = tempPath;
+                }
+
+                Directory.Move(currentPath, newPath);
+            }
+            finally
+            {
+                CollectionFolder.OnCollectionFolderChange();
+
+                Task.Run(() =>
+                {
+                    // No need to start if scanning the library because it will handle it
+                    if (refreshLibrary)
+                    {
+                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                    }
+                    else
+                    {
+                        // Need to add a delay here or directory watchers may still pick up the changes
+                        var task = Task.Delay(1000);
+                        // Have to block here to allow exceptions to bubble
+                        Task.WaitAll(task);
+
+                        _libraryMonitor.Start();
+                    }
+                });
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Add a media path to a library.
+        /// </summary>
+        /// <param name="name">The name of the library.</param>
+        /// <param name="path">The path to add.</param>
+        /// <param name="pathInfo">The path info.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        /// <response code="204">Media path added.</response>
+        /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+        [HttpPost("Paths")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult AddMediaPath(
+            [FromQuery] string name,
+            [FromQuery] string path,
+            [FromQuery] MediaPathInfo pathInfo,
+            [FromQuery] bool refreshLibrary)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            _libraryMonitor.Stop();
+
+            try
+            {
+                var mediaPath = pathInfo ?? new MediaPathInfo { Path = path };
+
+                _libraryManager.AddMediaPath(name, mediaPath);
+            }
+            finally
+            {
+                Task.Run(() =>
+                {
+                    // No need to start if scanning the library because it will handle it
+                    if (refreshLibrary)
+                    {
+                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                    }
+                    else
+                    {
+                        // Need to add a delay here or directory watchers may still pick up the changes
+                        var task = Task.Delay(1000);
+                        // Have to block here to allow exceptions to bubble
+                        Task.WaitAll(task);
+
+                        _libraryMonitor.Start();
+                    }
+                });
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates a media path.
+        /// </summary>
+        /// <param name="name">The name of the library.</param>
+        /// <param name="pathInfo">The path info.</param>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        /// <response code="204">Media path updated.</response>
+        /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+        [HttpPost("Paths/Update")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult UpdateMediaPath(
+            [FromQuery] string name,
+            [FromQuery] MediaPathInfo pathInfo)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            _libraryManager.UpdateMediaPath(name, pathInfo);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Remove a media path.
+        /// </summary>
+        /// <param name="name">The name of the library.</param>
+        /// <param name="path">The path to remove.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        /// <response code="204">Media path removed.</response>
+        /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+        [HttpDelete("Paths")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult RemoveMediaPath(
+            [FromQuery] string name,
+            [FromQuery] string path,
+            [FromQuery] bool refreshLibrary)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            _libraryMonitor.Stop();
+
+            try
+            {
+                _libraryManager.RemoveMediaPath(name, path);
+            }
+            finally
+            {
+                Task.Run(() =>
+                {
+                    // No need to start if scanning the library because it will handle it
+                    if (refreshLibrary)
+                    {
+                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                    }
+                    else
+                    {
+                        // Need to add a delay here or directory watchers may still pick up the changes
+                        var task = Task.Delay(1000);
+                        // Have to block here to allow exceptions to bubble
+                        Task.WaitAll(task);
+
+                        _libraryMonitor.Start();
+                    }
+                });
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Update library options.
+        /// </summary>
+        /// <param name="id">The library name.</param>
+        /// <param name="libraryOptions">The library options.</param>
+        /// <response code="204">Library updated.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("LibraryOptions")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult UpdateLibraryOptions(
+            [FromQuery] string id,
+            [FromQuery] LibraryOptions libraryOptions)
+        {
+            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
+
+            collectionFolder.UpdateLibraryOptions(libraryOptions);
+            return NoContent();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs
deleted file mode 100644
index 1e300814f6..0000000000
--- a/MediaBrowser.Api/Library/LibraryStructureService.cs
+++ /dev/null
@@ -1,412 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Library
-{
-    /// <summary>
-    /// Class GetDefaultVirtualFolders
-    /// </summary>
-    [Route("/Library/VirtualFolders", "GET")]
-    public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        public string UserId { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders", "POST")]
-    public class AddVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the collection.
-        /// </summary>
-        /// <value>The type of the collection.</value>
-        public string CollectionType { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public string[] Paths { get; set; }
-
-        public LibraryOptions LibraryOptions { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders", "DELETE")]
-    public class RemoveVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Name", "POST")]
-    public class RenameVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string NewName { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths", "POST")]
-    public class AddMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Path { get; set; }
-
-        public MediaPathInfo PathInfo { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths/Update", "POST")]
-    public class UpdateMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        public MediaPathInfo PathInfo { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths", "DELETE")]
-    public class RemoveMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/LibraryOptions", "POST")]
-    public class UpdateLibraryOptions : IReturnVoid
-    {
-        public string Id { get; set; }
-
-        public LibraryOptions LibraryOptions { get; set; }
-    }
-
-    /// <summary>
-    /// Class LibraryStructureService
-    /// </summary>
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class LibraryStructureService : BaseApiService
-    {
-        /// <summary>
-        /// The _app paths
-        /// </summary>
-        private readonly IServerApplicationPaths _appPaths;
-
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILibraryMonitor _libraryMonitor;
-
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryStructureService" /> class.
-        /// </summary>
-        public LibraryStructureService(
-            ILogger<LibraryStructureService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            ILibraryMonitor libraryMonitor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _libraryManager = libraryManager;
-            _libraryMonitor = libraryMonitor;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetVirtualFolders request)
-        {
-            var result = _libraryManager.GetVirtualFolders(true);
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Post(UpdateLibraryOptions request)
-        {
-            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
-
-            collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(AddVirtualFolder request)
-        {
-            var libraryOptions = request.LibraryOptions ?? new LibraryOptions();
-
-            if (request.Paths != null && request.Paths.Length > 0)
-            {
-                libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
-            }
-
-            return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RenameVirtualFolder request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            if (string.IsNullOrWhiteSpace(request.NewName))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            var rootFolderPath = _appPaths.DefaultUserViewsPath;
-
-            var currentPath = Path.Combine(rootFolderPath, request.Name);
-            var newPath = Path.Combine(rootFolderPath, request.NewName);
-
-            if (!Directory.Exists(currentPath))
-            {
-                throw new FileNotFoundException("The media collection does not exist");
-            }
-
-            if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
-            {
-                throw new ArgumentException("Media library already exists at " + newPath + ".");
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                // Changing capitalization. Handle windows case insensitivity
-                if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
-                {
-                    var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
-                    Directory.Move(currentPath, tempPath);
-                    currentPath = tempPath;
-                }
-
-                Directory.Move(currentPath, newPath);
-            }
-            finally
-            {
-                CollectionFolder.OnCollectionFolderChange();
-
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(RemoveVirtualFolder request)
-        {
-            return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(AddMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                var mediaPath = request.PathInfo ?? new MediaPathInfo
-                {
-                    Path = request.Path
-                };
-
-                _libraryManager.AddMediaPath(request.Name, mediaPath);
-            }
-            finally
-            {
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryManager.UpdateMediaPath(request.Name, request.PathInfo);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(RemoveMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                _libraryManager.RemoveMediaPath(request.Name, request.Path);
-            }
-            finally
-            {
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-    }
-}

From fff3c789b98aad08f8ea66da275ff82c71dd8f2b Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 12 Jun 2020 18:54:25 +0200
Subject: [PATCH 175/463] Move SessionService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/SessionController.cs | 478 +++++++++++++++++
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  15 +
 MediaBrowser.Api/Sessions/SessionService.cs   | 498 ------------------
 3 files changed, 493 insertions(+), 498 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SessionController.cs
 delete mode 100644 MediaBrowser.Api/Sessions/SessionService.cs

diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
new file mode 100644
index 0000000000..5b60275eb6
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -0,0 +1,478 @@
+#pragma warning disable CA1801
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The session controller.
+    /// </summary>
+    public class SessionController : BaseJellyfinApiController
+    {
+        private readonly ISessionManager _sessionManager;
+        private readonly IUserManager _userManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ISessionContext _sessionContext;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionController"/> class.
+        /// </summary>
+        /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+        /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="sessionContext">Instance of <see cref="ISessionContext"/> interface.</param>
+        public SessionController(
+            ISessionManager sessionManager,
+            IUserManager userManager,
+            IAuthorizationContext authContext,
+            IDeviceManager deviceManager,
+            ISessionContext sessionContext)
+        {
+            _sessionManager = sessionManager;
+            _userManager = userManager;
+            _authContext = authContext;
+            _deviceManager = deviceManager;
+            _sessionContext = sessionContext;
+        }
+
+        /// <summary>
+        /// Gets a list of sessions.
+        /// </summary>
+        /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
+        /// <param name="deviceId">Filter by device Id.</param>
+        /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
+        /// <response code="200">List of sessions returned.</response>
+        /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
+        [HttpGet("/Sessions")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+            [FromQuery] Guid controllableByUserId,
+            [FromQuery] string deviceId,
+            [FromQuery] int? activeWithinSeconds)
+        {
+            var result = _sessionManager.Sessions;
+
+            if (!string.IsNullOrEmpty(deviceId))
+            {
+                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+            }
+
+            if (!controllableByUserId.Equals(Guid.Empty))
+            {
+                result = result.Where(i => i.SupportsRemoteControl);
+
+                var user = _userManager.GetUserById(controllableByUserId);
+
+                if (!user.Policy.EnableRemoteControlOfOtherUsers)
+                {
+                    result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId));
+                }
+
+                if (!user.Policy.EnableSharedDeviceControl)
+                {
+                    result = result.Where(i => !i.UserId.Equals(Guid.Empty));
+                }
+
+                if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+                {
+                    var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+                    result = result.Where(i => i.LastActivityDate >= minActiveDate);
+                }
+
+                result = result.Where(i =>
+                {
+                    if (!string.IsNullOrWhiteSpace(i.DeviceId))
+                    {
+                        if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
+                        {
+                            return false;
+                        }
+                    }
+
+                    return true;
+                });
+            }
+
+            return Ok(result);
+        }
+
+        /// <summary>
+        /// Instructs a session to browse to an item or view.
+        /// </summary>
+        /// <param name="id">The session Id.</param>
+        /// <param name="itemType">The type of item to browse to.</param>
+        /// <param name="itemId">The Id of the item.</param>
+        /// <param name="itemName">The name of the item.</param>
+        /// <response code="204">Instruction sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Viewing")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult DisplayContent(
+            [FromRoute] string id,
+            [FromQuery] string itemType,
+            [FromQuery] string itemId,
+            [FromQuery] string itemName)
+        {
+            var command = new BrowseRequest
+            {
+                ItemId = itemId,
+                ItemName = itemName,
+                ItemType = itemType
+            };
+
+            _sessionManager.SendBrowseCommand(
+                RequestHelpers.GetSession(_sessionContext).Id,
+                id,
+                command,
+                CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Instructs a session to play an item.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="itemIds">The ids of the items to play, comma delimited.</param>
+        /// <param name="startPositionTicks">The starting position of the first item.</param>
+        /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
+        /// <param name="playRequest">The <see cref="PlayRequest"/>.</param>
+        /// <response code="204">Instruction sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Playing")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult Play(
+            [FromRoute] string id,
+            [FromQuery] Guid[] itemIds,
+            [FromQuery] long? startPositionTicks,
+            [FromQuery] PlayCommand playCommand,
+            [FromBody, Required] PlayRequest playRequest)
+        {
+            if (playRequest == null)
+            {
+                throw new ArgumentException("Request Body may not be null");
+            }
+
+            playRequest.ItemIds = itemIds;
+            playRequest.StartPositionTicks = startPositionTicks;
+            playRequest.PlayCommand = playCommand;
+
+            _sessionManager.SendPlayCommand(
+                RequestHelpers.GetSession(_sessionContext).Id,
+                id,
+                playRequest,
+                CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Issues a playstate command to a client.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
+        /// <response code="204">Playstate command sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Playing/{command}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SendPlaystateCommand(
+            [FromRoute] string id,
+            [FromBody] PlaystateRequest playstateRequest)
+        {
+            _sessionManager.SendPlaystateCommand(
+                RequestHelpers.GetSession(_sessionContext).Id,
+                id,
+                playstateRequest,
+                CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Issues a system command to a client.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="command">The command to send.</param>
+        /// <response code="204">System command sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/System/{Command}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SendSystemCommand(
+            [FromRoute] string id,
+            [FromRoute] string command)
+        {
+            var name = command;
+            if (Enum.TryParse(name, true, out GeneralCommandType commandType))
+            {
+                name = commandType.ToString();
+            }
+
+            var currentSession = RequestHelpers.GetSession(_sessionContext);
+            var generalCommand = new GeneralCommand
+            {
+                Name = name,
+                ControllingUserId = currentSession.UserId
+            };
+
+            _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Issues a general command to a client.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="command">The command to send.</param>
+        /// <response code="204">General command sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Command/{Command}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SendGeneralCommand(
+            [FromRoute] string id,
+            [FromRoute] string command)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionContext);
+
+            var generalCommand = new GeneralCommand
+            {
+                Name = command,
+                ControllingUserId = currentSession.UserId
+            };
+
+            _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Issues a full general command to a client.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="command">The <see cref="GeneralCommand"/>.</param>
+        /// <response code="204">Full general command sent to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Command")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SendFullGeneralCommand(
+            [FromRoute] string id,
+            [FromBody, Required] GeneralCommand command)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionContext);
+
+            if (command == null)
+            {
+                throw new ArgumentException("Request body may not be null");
+            }
+
+            command.ControllingUserId = currentSession.UserId;
+
+            _sessionManager.SendGeneralCommand(
+                currentSession.Id,
+                id,
+                command,
+                CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Issues a command to a client to display a message to the user.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="text">The message test.</param>
+        /// <param name="header">The message header.</param>
+        /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
+        /// <response code="204">Message sent.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/Message")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SendMessageCommand(
+            [FromRoute] string id,
+            [FromQuery] string text,
+            [FromQuery] string header,
+            [FromQuery] long? timeoutMs)
+        {
+            var command = new MessageCommand
+            {
+                Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
+                TimeoutMs = timeoutMs,
+                Text = text
+            };
+
+            _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Adds an additional user to a session.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <response code="204">User added to session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/{id}/User/{userId}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult AddUserToSession(
+            [FromRoute] string id,
+            [FromRoute] Guid userId)
+        {
+            _sessionManager.AddAdditionalUser(id, userId);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Removes an additional user from a session.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <response code="204">User removed from session.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Sessions/{id}/User/{userId}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult RemoveUserFromSession(
+            [FromRoute] string id,
+            [FromRoute] Guid userId)
+        {
+            _sessionManager.RemoveAdditionalUser(id, userId);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates capabilities for a device.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param>
+        /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param>
+        /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param>
+        /// <param name="supportsSync">Determines whether sync is supported.</param>
+        /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
+        /// <response code="204">Capabilities posted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Capabilities")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult PostCapabilities(
+            [FromQuery] string id,
+            [FromQuery] string playableMediaTypes,
+            [FromQuery] string supportedCommands,
+            [FromQuery] bool supportsMediaControl,
+            [FromQuery] bool supportsSync,
+            [FromQuery] bool supportsPersistentIdentifier = true)
+        {
+            if (string.IsNullOrWhiteSpace(id))
+            {
+                id = RequestHelpers.GetSession(_sessionContext).Id;
+            }
+
+            _sessionManager.ReportCapabilities(id, new ClientCapabilities
+            {
+                PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
+                SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+                SupportsMediaControl = supportsMediaControl,
+                SupportsSync = supportsSync,
+                SupportsPersistentIdentifier = supportsPersistentIdentifier
+            });
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates capabilities for a device.
+        /// </summary>
+        /// <param name="id">The session id.</param>
+        /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
+        /// <response code="204">Capabilities updated.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Capabilities/Full")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult PostFullCapabilities(
+            [FromQuery] string id,
+            [FromBody, Required] ClientCapabilities capabilities)
+        {
+            if (string.IsNullOrWhiteSpace(id))
+            {
+                id = RequestHelpers.GetSession(_sessionContext).Id;
+            }
+
+            _sessionManager.ReportCapabilities(id, capabilities);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that a session is viewing an item.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="204">Session reported to server.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Viewing")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult ReportViewing(
+            [FromQuery] string sessionId,
+            [FromQuery] string itemId)
+        {
+            string session = RequestHelpers.GetSession(_sessionContext).Id;
+
+            _sessionManager.ReportNowViewingItem(session, itemId);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that a session has ended.
+        /// </summary>
+        /// <response code="204">Session end reported to server.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Logout")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult ReportSessionEnded()
+        {
+            // TODO: how do we get AuthorizationInfo without an IRequest?
+            AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
+
+            _sessionManager.Logout(auth.Token);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Get all auth providers.
+        /// </summary>
+        /// <response code="200">Auth providers retrieved.</response>
+        /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
+        [HttpGet("/Auth/Providers")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
+        {
+            return _userManager.GetAuthenticationProviders();
+        }
+
+        /// <summary>
+        /// Get all password reset providers.
+        /// </summary>
+        /// <response code="200">Password reset providers retrieved.</response>
+        /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
+        [HttpGet("/Auto/PasswordResetProviders")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
+        {
+            return _userManager.GetPasswordResetProviders();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 9f4d34f9c6..ae8ab37e8e 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,4 +1,6 @@
 using System;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
 
 namespace Jellyfin.Api.Helpers
 {
@@ -25,5 +27,18 @@ namespace Jellyfin.Api.Helpers
                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
                 : value.Split(separator);
         }
+
+        internal static SessionInfo GetSession(ISessionContext sessionContext)
+        {
+            // TODO: how do we get a SessionInfo without IRequest?
+            SessionInfo session = sessionContext.GetSession("Request");
+
+            if (session == null)
+            {
+                throw new ArgumentException("Session not found.");
+            }
+
+            return session;
+        }
     }
 }
diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs
deleted file mode 100644
index 020bb5042b..0000000000
--- a/MediaBrowser.Api/Sessions/SessionService.cs
+++ /dev/null
@@ -1,498 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
-    /// <summary>
-    /// Class GetSessions.
-    /// </summary>
-    [Route("/Sessions", "GET", Summary = "Gets a list of sessions")]
-    [Authenticated]
-    public class GetSessions : IReturn<SessionInfo[]>
-    {
-        [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid ControllableByUserId { get; set; }
-
-        [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string DeviceId { get; set; }
-
-        public int? ActiveWithinSeconds { get; set; }
-    }
-
-    /// <summary>
-    /// Class DisplayContent.
-    /// </summary>
-    [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
-    [Authenticated]
-    public class DisplayContent : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Artist, Genre, Studio, Person, or any kind of BaseItem
-        /// </summary>
-        /// <value>The type of the item.</value>
-        [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemType { get; set; }
-
-        /// <summary>
-        /// Artist name, genre name, item Id, etc
-        /// </summary>
-        /// <value>The item identifier.</value>
-        [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the item.
-        /// </summary>
-        /// <value>The name of the item.</value>
-        [ApiMember(Name = "ItemName", Description = "The name of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemName { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")]
-    [Authenticated]
-    public class Play : PlayRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")]
-    [Authenticated]
-    public class SendPlaystateCommand : PlaystateRequest, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendSystemCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the command.
-        /// </summary>
-        /// <value>The play command.</value>
-        [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Command { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendGeneralCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the command.
-        /// </summary>
-        /// <value>The play command.</value>
-        [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Command { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendFullGeneralCommand : GeneralCommand, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")]
-    [Authenticated]
-    public class SendMessageCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Text", Description = "The message text.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Text { get; set; }
-
-        [ApiMember(Name = "Header", Description = "The message header.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Header { get; set; }
-
-        [ApiMember(Name = "TimeoutMs", Description = "The message timeout. If omitted the user will have to confirm viewing the message.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public long? TimeoutMs { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")]
-    [Authenticated]
-    public class AddUserToSession : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")]
-    [Authenticated]
-    public class RemoveUserFromSession : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
-    [Authenticated]
-    public class PostCapabilities : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlayableMediaTypes { get; set; }
-
-        [ApiMember(Name = "SupportedCommands", Description = "A list of supported remote control commands, comma delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string SupportedCommands { get; set; }
-
-        [ApiMember(Name = "SupportsMediaControl", Description = "Determines whether media can be played remotely.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsMediaControl { get; set; }
-
-        [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsSync { get; set; }
-
-        [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsPersistentIdentifier { get; set; }
-
-        public PostCapabilities()
-        {
-            SupportsPersistentIdentifier = true;
-        }
-    }
-
-    [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")]
-    [Authenticated]
-    public class PostFullCapabilities : ClientCapabilities, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")]
-    [Authenticated]
-    public class ReportViewing : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-    }
-
-    [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
-    [Authenticated]
-    public class ReportSessionEnded : IReturnVoid
-    {
-    }
-
-    [Route("/Auth/Providers", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetAuthProviders : IReturn<NameIdPair[]>
-    {
-    }
-
-    [Route("/Auth/PasswordResetProviders", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPasswordResetProviders : IReturn<NameIdPair[]>
-    {
-    }
-
-    /// <summary>
-    /// Class SessionsService.
-    /// </summary>
-    public class SessionService : BaseApiService
-    {
-        /// <summary>
-        /// The session manager.
-        /// </summary>
-        private readonly ISessionManager _sessionManager;
-
-        private readonly IUserManager _userManager;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IDeviceManager _deviceManager;
-        private readonly ISessionContext _sessionContext;
-
-        public SessionService(
-            ILogger<SessionService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISessionManager sessionManager,
-            IUserManager userManager,
-            IAuthorizationContext authContext,
-            IDeviceManager deviceManager,
-            ISessionContext sessionContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _sessionManager = sessionManager;
-            _userManager = userManager;
-            _authContext = authContext;
-            _deviceManager = deviceManager;
-            _sessionContext = sessionContext;
-        }
-
-        public object Get(GetAuthProviders request)
-        {
-            return _userManager.GetAuthenticationProviders();
-        }
-
-        public object Get(GetPasswordResetProviders request)
-        {
-            return _userManager.GetPasswordResetProviders();
-        }
-
-        public void Post(ReportSessionEnded request)
-        {
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            _sessionManager.Logout(auth.Token);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSessions request)
-        {
-            var result = _sessionManager.Sessions;
-
-            if (!string.IsNullOrEmpty(request.DeviceId))
-            {
-                result = result.Where(i => string.Equals(i.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase));
-            }
-
-            if (!request.ControllableByUserId.Equals(Guid.Empty))
-            {
-                result = result.Where(i => i.SupportsRemoteControl);
-
-                var user = _userManager.GetUserById(request.ControllableByUserId);
-
-                if (!user.Policy.EnableRemoteControlOfOtherUsers)
-                {
-                    result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
-                }
-
-                if (!user.Policy.EnableSharedDeviceControl)
-                {
-                    result = result.Where(i => !i.UserId.Equals(Guid.Empty));
-                }
-
-                if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0)
-                {
-                    var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value);
-                    result = result.Where(i => i.LastActivityDate >= minActiveDate);
-                }
-
-                result = result.Where(i =>
-                {
-                    var deviceId = i.DeviceId;
-
-                    if (!string.IsNullOrWhiteSpace(deviceId))
-                    {
-                        if (!_deviceManager.CanAccessDevice(user, deviceId))
-                        {
-                            return false;
-                        }
-                    }
-
-                    return true;
-                });
-            }
-
-            return ToOptimizedResult(result.ToArray());
-        }
-
-        public Task Post(SendPlaystateCommand request)
-        {
-            return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(DisplayContent request)
-        {
-            var command = new BrowseRequest
-            {
-                ItemId = request.ItemId,
-                ItemName = request.ItemName,
-                ItemType = request.ItemType
-            };
-
-            return _sessionManager.SendBrowseCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(SendSystemCommand request)
-        {
-            var name = request.Command;
-            if (Enum.TryParse(name, true, out GeneralCommandType commandType))
-            {
-                name = commandType.ToString();
-            }
-
-            var currentSession = GetSession(_sessionContext);
-            var command = new GeneralCommand
-            {
-                Name = name,
-                ControllingUserId = currentSession.UserId
-            };
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(SendMessageCommand request)
-        {
-            var command = new MessageCommand
-            {
-                Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header,
-                TimeoutMs = request.TimeoutMs,
-                Text = request.Text
-            };
-
-            return _sessionManager.SendMessageCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(Play request)
-        {
-            return _sessionManager.SendPlayCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
-        }
-
-        public Task Post(SendGeneralCommand request)
-        {
-            var currentSession = GetSession(_sessionContext);
-
-            var command = new GeneralCommand
-            {
-                Name = request.Command,
-                ControllingUserId = currentSession.UserId
-            };
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
-        }
-
-        public Task Post(SendFullGeneralCommand request)
-        {
-            var currentSession = GetSession(_sessionContext);
-
-            request.ControllingUserId = currentSession.UserId;
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, request, CancellationToken.None);
-        }
-
-        public void Post(AddUserToSession request)
-        {
-            _sessionManager.AddAdditionalUser(request.Id, new Guid(request.UserId));
-        }
-
-        public void Delete(RemoveUserFromSession request)
-        {
-            _sessionManager.RemoveAdditionalUser(request.Id, new Guid(request.UserId));
-        }
-
-        public void Post(PostCapabilities request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Id))
-            {
-                request.Id = GetSession(_sessionContext).Id;
-            }
-
-            _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities
-            {
-                PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','),
-                SupportedCommands = SplitValue(request.SupportedCommands, ','),
-                SupportsMediaControl = request.SupportsMediaControl,
-                SupportsSync = request.SupportsSync,
-                SupportsPersistentIdentifier = request.SupportsPersistentIdentifier
-            });
-        }
-
-        public void Post(PostFullCapabilities request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Id))
-            {
-                request.Id = GetSession(_sessionContext).Id;
-            }
-
-            _sessionManager.ReportCapabilities(request.Id, request);
-        }
-
-        public void Post(ReportViewing request)
-        {
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId);
-        }
-    }
-}

From 720fff30a4da7490ce2ce6053cb496dbf19d6c8f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 12 Jun 2020 14:37:55 -0600
Subject: [PATCH 176/463] readd swagger version

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 0097462430..c3c8716c0b 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -119,7 +119,7 @@ namespace Jellyfin.Server.Extensions
         {
             return serviceCollection.AddSwaggerGen(c =>
             {
-                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" });
+                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
                 c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
                 {
                     Type = SecuritySchemeType.ApiKey,

From 6d5c09c4990ecbc071afbe6611ecef75e5fe8b65 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 12 Jun 2020 14:40:06 -0600
Subject: [PATCH 177/463] Remove duplicate swaggerdoc

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index c3c8716c0b..86d547af04 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -139,7 +139,6 @@ namespace Jellyfin.Server.Extensions
                 {
                     { securitySchemeRef, Array.Empty<string>() }
                 });
-                c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
 
                 // Add all xml doc files to swagger generator.
                 var xmlFiles = Directory.GetFiles(

From ec3e15db5789b6218482beb488433f41f9a0d8ba Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 13:11:41 -0600
Subject: [PATCH 178/463] Fix merge and build

---
 Jellyfin.Api/BaseJellyfinApiController.cs     |  4 --
 .../Controllers/ActivityLogController.cs      | 11 +++-
 .../ConfigurationDtos/MediaEncoderPathDto.cs  |  6 +-
 .../ApiServiceCollectionExtensions.cs         |  2 +-
 .../CamelCaseJsonProfileFormatter.cs          |  2 +-
 .../PascalCaseJsonProfileFormatter.cs         |  2 +-
 MediaBrowser.Api/MediaBrowser.Api.csproj      |  4 --
 MediaBrowser.Api/System/ActivityLogService.cs | 66 -------------------
 .../JsonNonStringKeyDictionaryConverter.cs    | 26 ++++----
 ...nNonStringKeyDictionaryConverterFactory.cs |  5 +-
 MediaBrowser.Common/Json/JsonDefaults.cs      | 30 ++++-----
 11 files changed, 46 insertions(+), 112 deletions(-)
 delete mode 100644 MediaBrowser.Api/System/ActivityLogService.cs

diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 615f330a4c..a34f9eb62f 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-using System;
-=======
 using System.Net.Mime;
->>>>>>> origin/master
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 8d37a83738..895d9f719d 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,6 +1,10 @@
+#nullable enable
+#pragma warning disable CA1801
+
 using System;
-using System.Globalization;
+using System.Linq;
 using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
@@ -44,7 +48,10 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] DateTime? minDate,
             bool? hasUserId)
         {
-            return _activityManager.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
+            var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
+                entries => entries.Where(entry => entry.DateCreated >= minDate));
+
+            return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
         }
     }
 }
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
index b05e0cdf5a..3706a11e3a 100644
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
 namespace Jellyfin.Api.Models.ConfigurationDtos
 {
     /// <summary>
@@ -8,11 +10,11 @@ namespace Jellyfin.Api.Models.ConfigurationDtos
         /// <summary>
         /// Gets or sets media encoder path.
         /// </summary>
-        public string Path { get; set; }
+        public string Path { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets media encoder path type.
         /// </summary>
-        public string PathType { get; set; }
+        public string PathType { get; set; } = null!;
     }
 }
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 86d547af04..9cdaa0eb16 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -93,7 +93,7 @@ namespace Jellyfin.Server.Extensions
                 .AddJsonOptions(options =>
                 {
                     // Update all properties that are set in JsonDefaults
-                    var jsonOptions = JsonDefaults.PascalCase;
+                    var jsonOptions = JsonDefaults.GetPascalCaseOptions();
 
                     // From JsonDefaults
                     options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
index 989c8ecea2..9b347ae2c2 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase)
+        public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions())
         {
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
index 69963b3fb3..0024708bad 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
         /// </summary>
-        public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase)
+        public PascalCaseJsonProfileFormatter() : base(JsonDefaults.GetPascalCaseOptions())
         {
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index 0b0c5cc9fc..d703bdb058 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -14,10 +14,6 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Attachments" />
-  </ItemGroup>
-
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
deleted file mode 100644
index a6bacad4fc..0000000000
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
-    [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
-    public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
-    {
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDate { get; set; }
-
-        public bool? HasUserId { get; set; }
-    }
-
-    [Authenticated(Roles = "Admin")]
-    public class ActivityLogService : BaseApiService
-    {
-        private readonly IActivityManager _activityManager;
-
-        public ActivityLogService(
-            ILogger<ActivityLogService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IActivityManager activityManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _activityManager = activityManager;
-        }
-
-        public object Get(GetActivityLogs request)
-        {
-            DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
-                (DateTime?)null :
-                DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
-            var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
-                entries => entries.Where(entry => entry.DateCreated >= minDate));
-
-            var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
-
-            return ToOptimizedResult(result);
-        }
-    }
-}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
index 636ef5372f..8053461f08 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Json.Converters
         /// <param name="typeToConvert">The type to convert.</param>
         /// <param name="options">The json serializer options.</param>
         /// <returns>Typed dictionary.</returns>
-        /// <exception cref="NotSupportedException"></exception>
+        /// <exception cref="NotSupportedException">Dictionary key type not supported.</exception>
         public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
             var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]);
@@ -38,24 +38,24 @@ namespace MediaBrowser.Common.Json.Converters
                 CultureInfo.CurrentCulture);
             var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null);
             var parse = typeof(TKey).GetMethod(
-                "Parse", 
-                0, 
-                BindingFlags.Public | BindingFlags.Static, 
-                null, 
-                CallingConventions.Any, 
-                new[] { typeof(string) }, 
+                "Parse",
+                0,
+                BindingFlags.Public | BindingFlags.Static,
+                null,
+                CallingConventions.Any,
+                new[] { typeof(string) },
                 null);
             if (parse == null)
             {
                 throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary<TKey, TValue> is not supported.");
             }
-            
+
             while (enumerator.MoveNext())
             {
                 var element = (KeyValuePair<string?, TValue>)enumerator.Current;
-                instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value);
+                instance.Add((TKey)parse.Invoke(null, new[] { (object?)element.Key }), element.Value);
             }
-            
+
             return instance;
         }
 
@@ -70,8 +70,12 @@ namespace MediaBrowser.Common.Json.Converters
             var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
             foreach (var (k, v) in value)
             {
-                convertedDictionary[k?.ToString()] = v;
+                if (k != null)
+                {
+                    convertedDictionary[k.ToString()] = v;
+                }
             }
+
             JsonSerializer.Serialize(writer, convertedDictionary, options);
         }
     }
diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
index d9795a189a..52f3607401 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs
@@ -22,18 +22,17 @@ namespace MediaBrowser.Common.Json.Converters
         /// <returns>Conversion ability.</returns>
         public override bool CanConvert(Type typeToConvert)
         {
-            
             if (!typeToConvert.IsGenericType)
             {
                 return false;
             }
-            
+
             // Let built in converter handle string keys
             if (typeToConvert.GenericTypeArguments[0] == typeof(string))
             {
                 return false;
             }
-            
+
             // Only support objects that implement IDictionary
             return typeToConvert.GetInterface(nameof(IDictionary)) != null;
         }
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index f38e2893ec..adc15123b1 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Common.Json
         /// When changing these options, update
         ///     Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
         ///         -> AddJellyfinApi
-        ///             -> AddJsonOptions
+        ///             -> AddJsonOptions.
         /// </remarks>
         /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
         public static JsonSerializerOptions GetOptions()
@@ -33,31 +33,27 @@ namespace MediaBrowser.Common.Json
 
             return options;
         }
-        
+
         /// <summary>
-        /// Gets CamelCase json options.
+        /// Gets camelCase json options.
         /// </summary>
-        public static JsonSerializerOptions CamelCase
+        /// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
+        public static JsonSerializerOptions GetCamelCaseOptions()
         {
-            get
-            {
-                var options = GetOptions();
-                options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-                return options;
-            }
+            var options = GetOptions();
+            options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+            return options;
         }
 
         /// <summary>
         /// Gets PascalCase json options.
         /// </summary>
-        public static JsonSerializerOptions PascalCase
+        /// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
+        public static JsonSerializerOptions GetPascalCaseOptions()
         {
-            get
-            {
-                var options = GetOptions();
-                options.PropertyNamingPolicy = null;
-                return options;
-            }
+            var options = GetOptions();
+            options.PropertyNamingPolicy = null;
+            return options;
         }
     }
 }

From 552a74eb6e874976116447754785b6c1ca355718 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 15:13:57 -0600
Subject: [PATCH 179/463] Fix build

---
 Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index 1155cc653e..f521dfdf28 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -85,9 +85,8 @@ namespace Jellyfin.Api.Controllers.Images
 
             var images = await _providerManager.GetAvailableRemoteImages(
                     item,
-                    new RemoteImageQuery
+                    new RemoteImageQuery(providerName)
                     {
-                        ProviderName = providerName,
                         IncludeAllLanguages = includeAllLanguages,
                         IncludeDisabledProviders = true,
                         ImageType = type

From 3c18745f5392d45c1f008f15438e91831fb39294 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 15:15:27 -0600
Subject: [PATCH 180/463] Remove RemoteImageService.cs

---
 MediaBrowser.Api/Images/RemoteImageService.cs | 297 ------------------
 1 file changed, 297 deletions(-)
 delete mode 100644 MediaBrowser.Api/Images/RemoteImageService.cs

diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
deleted file mode 100644
index 358ac30fae..0000000000
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ /dev/null
@@ -1,297 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
-    public class BaseRemoteImageRequest : IReturn<RemoteImageResult>
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public ImageType? Type { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeAllLanguages { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")]
-    [Authenticated]
-    public class GetRemoteImages : BaseRemoteImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")]
-    [Authenticated]
-    public class GetRemoteImageProviders : IReturn<List<ImageProviderInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    public class BaseDownloadRemoteImage : IReturnVoid
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public ImageType Type { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ImageUrl { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")]
-    [Authenticated(Roles = "Admin")]
-    public class DownloadRemoteImage : BaseDownloadRemoteImage
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Images/Remote", "GET", Summary = "Gets a remote image")]
-    public class GetRemoteImage
-    {
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ImageUrl { get; set; }
-    }
-
-    public class RemoteImageService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IHttpClient _httpClient;
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ILibraryManager _libraryManager;
-
-        public RemoteImageService(
-            ILogger<RemoteImageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            IServerApplicationPaths appPaths,
-            IHttpClient httpClient,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _appPaths = appPaths;
-            _httpClient = httpClient;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetRemoteImageProviders request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var result = GetImageProviders(item);
-
-            return ToOptimizedResult(result);
-        }
-
-        private List<ImageProviderInfo> GetImageProviders(BaseItem item)
-        {
-            return _providerManager.GetRemoteImageProviderInfo(item).ToList();
-        }
-
-        public async Task<object> Get(GetRemoteImages request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery(request.ProviderName)
-            {
-                IncludeAllLanguages = request.IncludeAllLanguages,
-                IncludeDisabledProviders = true,
-                ImageType = request.Type
-
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            var imagesList = images.ToArray();
-
-            var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
-
-            if (request.Type.HasValue)
-            {
-                allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value));
-            }
-
-            var result = new RemoteImageResult
-            {
-                TotalRecordCount = imagesList.Length,
-                Providers = allProviders.Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToArray()
-            };
-
-            if (request.StartIndex.HasValue)
-            {
-                imagesList = imagesList.Skip(request.StartIndex.Value)
-                    .ToArray();
-            }
-
-            if (request.Limit.HasValue)
-            {
-                imagesList = imagesList.Take(request.Limit.Value)
-                    .ToArray();
-            }
-
-            result.Images = imagesList;
-
-            return result;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(DownloadRemoteImage request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return DownloadRemoteImage(item, request);
-        }
-
-        /// <summary>
-        /// Downloads the remote image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="request">The request.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request)
-        {
-            await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false);
-
-            item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetRemoteImage request)
-        {
-            var urlHash = request.ImageUrl.GetMD5();
-            var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
-            string contentPath;
-
-            try
-            {
-                contentPath = File.ReadAllText(pointerCachePath);
-
-                if (File.Exists(contentPath))
-                {
-                    return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // Means the file isn't cached yet
-            }
-            catch (IOException)
-            {
-                // Means the file isn't cached yet
-            }
-
-            await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
-            // Read the pointer file again
-            contentPath = File.ReadAllText(pointerCachePath);
-
-            return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Downloads the image.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <param name="urlHash">The URL hash.</param>
-        /// <param name="pointerCachePath">The pointer cache path.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
-        {
-            using var result = await _httpClient.GetResponse(new HttpRequestOptions
-            {
-                Url = url,
-                BufferContent = false
-            }).ConfigureAwait(false);
-            var ext = result.ContentType.Split('/')[^1];
-
-            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            var stream = result.Content;
-            await using (stream.ConfigureAwait(false))
-            {
-                var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-                await using (filestream.ConfigureAwait(false))
-                {
-                    await stream.CopyToAsync(filestream).ConfigureAwait(false);
-                }
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
-            File.WriteAllText(pointerCachePath, fullCachePath);
-        }
-
-        /// <summary>
-        /// Gets the full cache path.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        private string GetFullCachePath(string filename)
-        {
-            return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-        }
-    }
-}

From 2d4998c5782d426f09aa264f37a2bc384bf940f2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 15:28:04 -0600
Subject: [PATCH 181/463] fix build

---
 Jellyfin.Api/Controllers/EnvironmentController.cs | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 35cd89e0e8..046ffdf8eb 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -64,12 +64,7 @@ namespace Jellyfin.Api.Controllers
                     .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
                     .OrderBy(i => i.FullName);
 
-            return entries.Select(f => new FileSystemEntryInfo
-            {
-                Name = f.Name,
-                Path = f.FullName,
-                Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File
-            });
+            return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
         }
 
         /// <summary>
@@ -151,12 +146,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<FileSystemEntryInfo> GetDrives()
         {
-            return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo
-            {
-                Name = d.Name,
-                Path = d.FullName,
-                Type = FileSystemEntryType.Directory
-            });
+            return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
         }
 
         /// <summary>

From 13b53db4efd14926a519b9cd6006c51a3536565a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 15:31:22 -0600
Subject: [PATCH 182/463] fix build

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 1e9aa7b43c..6b2d014a1d 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="sortBy">Sort By. Comma delimited string.</param>
         /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
         /// <returns>Order By.</returns>
-        public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
+        public static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
         {
             var val = sortBy;
 
@@ -56,7 +56,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="fields">The fields.</param>
         /// <returns>IEnumerable{ItemFields}.</returns>
-        public static ItemFields[] GetItemFields(string fields)
+        public static ItemFields[] GetItemFields(string? fields)
         {
             if (string.IsNullOrEmpty(fields))
             {
@@ -79,7 +79,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="filters">The filters.</param>
         /// <returns>Item filters.</returns>
-        public static IEnumerable<ItemFilter> GetFilters(string filters)
+        public static IEnumerable<ItemFilter> GetFilters(string? filters)
         {
             return string.IsNullOrEmpty(filters)
                 ? Array.Empty<ItemFilter>()

From 276750f310b741b1610f9abaff66c204571c58ed Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 13 Jun 2020 15:33:42 -0600
Subject: [PATCH 183/463] Move ItemRefreshService.cs to Jellyfin.Api

---
 .../Controllers/ItemRefreshController.cs      | 89 +++++++++++++++++++
 MediaBrowser.Api/ItemRefreshService.cs        | 83 -----------------
 2 files changed, 89 insertions(+), 83 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ItemRefreshController.cs
 delete mode 100644 MediaBrowser.Api/ItemRefreshService.cs

diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
new file mode 100644
index 0000000000..d9b8357d2e
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -0,0 +1,89 @@
+#nullable enable
+#pragma warning disable CA1801
+
+using System.ComponentModel;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Item Refresh Controller.
+    /// </summary>
+    /// [Authenticated]
+    [Route("/Items")]
+    [Authorize]
+    public class ItemRefreshController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IProviderManager _providerManager;
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+        public ItemRefreshController(
+            ILibraryManager libraryManager,
+            IProviderManager providerManager,
+            IFileSystem fileSystem)
+        {
+            _libraryManager = libraryManager;
+            _providerManager = providerManager;
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Refreshes metadata for an item.
+        /// </summary>
+        /// <param name="id">Item id.</param>
+        /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
+        /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
+        /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
+        /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+        /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
+        /// <response code="200">Item metadata refresh queued.</response>
+        /// <response code="404">Item to refresh not found.</response>
+        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        [HttpPost("{Id}/Refresh")]
+        [Description("Refreshes metadata for an item.")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult Post(
+            [FromRoute] string id,
+            [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
+            [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
+            [FromQuery] bool replaceAllMetadata = false,
+            [FromQuery] bool replaceAllImages = false,
+            [FromQuery] bool recursive = false)
+        {
+            var item = _libraryManager.GetItemById(id);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+            {
+                MetadataRefreshMode = metadataRefreshMode,
+                ImageRefreshMode = imageRefreshMode,
+                ReplaceAllImages = replaceAllImages,
+                ReplaceAllMetadata = replaceAllMetadata,
+                ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
+                    || imageRefreshMode == MetadataRefreshMode.FullRefresh
+                    || replaceAllImages
+                    || replaceAllMetadata,
+                IsAutomated = false
+            };
+
+            _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
+            return Ok();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs
deleted file mode 100644
index 5e86f04a82..0000000000
--- a/MediaBrowser.Api/ItemRefreshService.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    public class BaseRefreshRequest : IReturnVoid
-    {
-        [ApiMember(Name = "MetadataRefreshMode", Description = "Specifies the metadata refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public MetadataRefreshMode MetadataRefreshMode { get; set; }
-
-        [ApiMember(Name = "ImageRefreshMode", Description = "Specifies the image refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public MetadataRefreshMode ImageRefreshMode { get; set; }
-
-        [ApiMember(Name = "ReplaceAllMetadata", Description = "Determines if metadata should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool ReplaceAllMetadata { get; set; }
-
-        [ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool ReplaceAllImages { get; set; }
-    }
-
-    [Route("/Items/{Id}/Refresh", "POST", Summary = "Refreshes metadata for an item")]
-    public class RefreshItem : BaseRefreshRequest
-    {
-        [ApiMember(Name = "Recursive", Description = "Indicates if the refresh should occur recursively.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool Recursive { get; set; }
-
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Authenticated]
-    public class ItemRefreshService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
-
-        public ItemRefreshService(
-            ILogger<ItemRefreshService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            IProviderManager providerManager,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RefreshItem request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var options = GetRefreshOptions(request);
-
-            _providerManager.QueueRefresh(item.Id, options, RefreshPriority.High);
-        }
-
-        private MetadataRefreshOptions GetRefreshOptions(RefreshItem request)
-        {
-            return new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-            {
-                MetadataRefreshMode = request.MetadataRefreshMode,
-                ImageRefreshMode = request.ImageRefreshMode,
-                ReplaceAllImages = request.ReplaceAllImages,
-                ReplaceAllMetadata = request.ReplaceAllMetadata,
-                ForceSave = request.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || request.ImageRefreshMode == MetadataRefreshMode.FullRefresh || request.ReplaceAllImages || request.ReplaceAllMetadata,
-                IsAutomated = false
-            };
-        }
-    }
-}

From edc5611ec7c638164ffe14e4c06055d4fd58b5e8 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 14 Jun 2020 13:50:51 +0200
Subject: [PATCH 184/463] Fix null reference to fix CI

---
 Jellyfin.Api/Controllers/LibraryStructureController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index f074a61dbe..ecbfed4693 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CA1801
+#pragma warning disable CA1801
 
 using System;
 using System.Collections.Generic;
@@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers
             ILibraryManager libraryManager,
             ILibraryMonitor libraryMonitor)
         {
-            _appPaths = serverConfigurationManager?.ApplicationPaths;
+            _appPaths = serverConfigurationManager.ApplicationPaths;
             _libraryManager = libraryManager;
             _libraryMonitor = libraryMonitor;
         }

From 524243a9340bfccd2c9ae708be70a5e49f5f53f1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 14 Jun 2020 20:18:06 -0600
Subject: [PATCH 185/463] fix merge conflict

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 5af1947562..a76675d5a9 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Models.NotificationDtos;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Dto;
@@ -115,7 +116,10 @@ namespace Jellyfin.Api.Controllers
                 Description = description,
                 Url = url,
                 Level = level ?? NotificationLevel.Normal,
-                UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
+                UserIds = _userManager.Users
+                    .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
+                    .Select(user => user.Id)
+                    .ToArray(),
                 Date = DateTime.UtcNow,
             };
 

From 4aac93672115d96ab77534f2b6a32a23482dab38 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 15 Jun 2020 12:49:54 -0600
Subject: [PATCH 186/463] Add more authorization handlers, actually authorize
 requests

---
 .../HttpServer/Security/AuthService.cs        |  45 ++++----
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 102 ++++++++++++++++++
 .../Auth/CustomAuthenticationHandler.cs       |  25 +++--
 .../DefaultAuthorizationHandler.cs            |  42 ++++++++
 .../DefaultAuthorizationRequirement.cs        |  11 ++
 .../FirstTimeSetupOrElevatedHandler.cs        |  21 +++-
 .../IgnoreScheduleHandler.cs                  |  42 ++++++++
 .../IgnoreScheduleRequirement.cs              |  11 ++
 .../LocalAccessPolicy/LocalAccessHandler.cs   |  44 ++++++++
 .../LocalAccessRequirement.cs                 |  11 ++
 .../RequiresElevationHandler.cs               |  26 ++++-
 Jellyfin.Api/Constants/InternalClaimTypes.cs  |  38 +++++++
 Jellyfin.Api/Constants/Policies.cs            |  15 +++
 Jellyfin.Api/Helpers/ClaimHelpers.cs          |  77 +++++++++++++
 .../ApiServiceCollectionExtensions.cs         |  31 +++++-
 MediaBrowser.Controller/Net/IAuthService.cs   |  25 ++++-
 16 files changed, 525 insertions(+), 41 deletions(-)
 create mode 100644 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
 create mode 100644 Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
 create mode 100644 Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
 create mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
 create mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
 create mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
 create mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
 create mode 100644 Jellyfin.Api/Constants/InternalClaimTypes.cs
 create mode 100644 Jellyfin.Api/Helpers/ClaimHelpers.cs

diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index c65d4694aa..6a2d8fdbbb 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _networkManager = networkManager;
         }
 
-        public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
         {
-            ValidateUser(request, authAttribtues);
+            ValidateUser(request, authAttributes);
         }
 
         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return user;
         }
 
-        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+        public AuthorizationInfo Authenticate(HttpRequest request)
+        {
+            var auth = _authorizationContext.GetAuthorizationInfo(request);
+            if (auth?.User == null)
+            {
+                return null;
+            }
+
+            if (auth.User.HasPermission(PermissionKind.IsDisabled))
+            {
+                throw new SecurityException("User account has been disabled.");
+            }
+
+            return auth;
+        }
+
+        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
         {
             // This code is executed before the service
             var auth = _authorizationContext.GetAuthorizationInfo(request);
 
-            if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+            if (!IsExemptFromAuthenticationToken(authAttributes, request))
             {
                 ValidateSecurityToken(request, auth.Token);
             }
 
-            if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+            if (authAttributes.AllowLocalOnly && !request.IsLocal)
             {
                 throw new SecurityException("Operation not found.");
             }
@@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (user != null)
             {
-                ValidateUserAccess(user, request, authAttribtues, auth);
+                ValidateUserAccess(user, request, authAttributes);
             }
 
             var info = GetTokenInfo(request);
 
-            if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+            if (!IsExemptFromRoles(auth, authAttributes, request, info))
             {
-                var roles = authAttribtues.GetRoles();
+                var roles = authAttributes.GetRoles();
 
                 ValidateRoles(roles, user);
             }
@@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private void ValidateUserAccess(
             User user,
             IRequest request,
-            IAuthenticationAttributes authAttributes,
-            AuthorizationInfo auth)
+            IAuthenticationAttributes authAttributes)
         {
             if (user.HasPermission(PermissionKind.IsDisabled))
             {
@@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
             {
                 throw new AuthenticationException("Access token is invalid or expired.");
             }
-
-            //if (!string.IsNullOrEmpty(info.UserId))
-            //{
-            //    var user = _userManager.GetUserById(info.UserId);
-
-            //    if (user == null || user.Configuration.IsDisabled)
-            //    {
-            //        throw new SecurityException("User account has been disabled.");
-            //    }
-            //}
         }
     }
 }
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
new file mode 100644
index 0000000000..b5b9d89041
--- /dev/null
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -0,0 +1,102 @@
+#nullable enable
+
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth
+{
+    /// <summary>
+    /// Base authorization handler.
+    /// </summary>
+    /// <typeparam name="T">Type of Authorization Requirement.</typeparam>
+    public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
+        where T : IAuthorizationRequirement
+    {
+        private readonly IUserManager _userManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IHttpContextAccessor _httpContextAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        protected BaseAuthorizationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+        {
+            _userManager = userManager;
+            _networkManager = networkManager;
+            _httpContextAccessor = httpContextAccessor;
+        }
+
+        /// <summary>
+        /// Validate authenticated claims.
+        /// </summary>
+        /// <param name="claimsPrincipal">Request claims.</param>
+        /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
+        /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
+        /// <returns>Validated claim status.</returns>
+        protected bool ValidateClaims(
+            ClaimsPrincipal claimsPrincipal,
+            bool ignoreSchedule = false,
+            bool localAccessOnly = false)
+        {
+            // Ensure claim has userId.
+            var userId = ClaimHelpers.GetUserId(claimsPrincipal);
+            if (userId == null)
+            {
+                return false;
+            }
+
+            // Ensure userId links to a valid user.
+            var user = _userManager.GetUserById(userId.Value);
+            if (user == null)
+            {
+                return false;
+            }
+
+            // Ensure user is not disabled.
+            if (user.HasPermission(PermissionKind.IsDisabled))
+            {
+                return false;
+            }
+
+            var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+            var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+            // User cannot access remotely and user is remote
+            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
+            {
+                return false;
+            }
+
+            if (localAccessOnly && !isInLocalNetwork)
+            {
+                return false;
+            }
+
+            // User attempting to access out of parental control hours.
+            if (!ignoreSchedule
+                && !user.HasPermission(PermissionKind.IsAdministrator)
+                && !user.IsParentalScheduleAllowed())
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private static IPAddress NormalizeIp(IPAddress ip)
+        {
+            return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index a5c4e9974a..d4d40da577 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,3 +1,6 @@
+#nullable enable
+
+using System.Globalization;
 using System.Security.Authentication;
 using System.Security.Claims;
 using System.Text.Encodings.Web;
@@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
         /// <inheritdoc />
         protected override Task<AuthenticateResult> HandleAuthenticateAsync()
         {
-            var authenticatedAttribute = new AuthenticatedAttribute
-            {
-                IgnoreLegacyAuth = true
-            };
-
             try
             {
-                var user = _authService.Authenticate(Request, authenticatedAttribute);
-                if (user == null)
+                var authorizationInfo = _authService.Authenticate(Request);
+                if (authorizationInfo == null)
                 {
                     return Task.FromResult(AuthenticateResult.NoResult());
                     // TODO return when legacy API is removed.
@@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
 
                 var claims = new[]
                 {
-                    new Claim(ClaimTypes.Name, user.Username),
-                    new Claim(
-                        ClaimTypes.Role,
-                        value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
+                    new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
+                    new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+                    new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
+                    new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
+                    new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
+                    new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
+                    new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+                    new Claim(InternalClaimTypes.Token, authorizationInfo.Token)
                 };
+
                 var identity = new ClaimsIdentity(claims, Scheme.Name);
                 var principal = new ClaimsPrincipal(identity);
                 var ticket = new AuthenticationTicket(principal, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
new file mode 100644
index 0000000000..b5913daab9
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+    /// <summary>
+    /// Default authorization handler.
+    /// </summary>
+    public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public DefaultAuthorizationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User);
+            if (!validated)
+            {
+                context.Fail();
+                return Task.CompletedTask;
+            }
+
+            context.Succeed(requirement);
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
new file mode 100644
index 0000000000..7cea00b694
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+    /// <summary>
+    /// The default authorization requirement.
+    /// </summary>
+    public class DefaultAuthorizationRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index 34aa5d12c8..0b12f7d3c2 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -1,22 +1,33 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
 {
     /// <summary>
     /// Authorization handler for requiring first time setup or elevated privileges.
     /// </summary>
-    public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+    public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
     {
         private readonly IConfigurationManager _configurationManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
         /// </summary>
-        /// <param name="configurationManager">The jellyfin configuration manager.</param>
-        public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public FirstTimeSetupOrElevatedHandler(
+            IConfigurationManager configurationManager,
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
         {
             _configurationManager = configurationManager;
         }
@@ -28,7 +39,9 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
             {
                 context.Succeed(firstTimeSetupOrElevatedRequirement);
             }
-            else if (context.User.IsInRole(UserRoles.Administrator))
+
+            var validated = ValidateClaims(context.User);
+            if (validated && context.User.IsInRole(UserRoles.Administrator))
             {
                 context.Succeed(firstTimeSetupOrElevatedRequirement);
             }
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
new file mode 100644
index 0000000000..9afa0b28f1
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+    /// <summary>
+    /// Escape schedule controls handler.
+    /// </summary>
+    public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public IgnoreScheduleHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, ignoreSchedule: true);
+            if (!validated)
+            {
+                context.Fail();
+                return Task.CompletedTask;
+            }
+
+            context.Succeed(requirement);
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
new file mode 100644
index 0000000000..d5bb61ce6c
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+    /// <summary>
+    /// Escape schedule controls requirement.
+    /// </summary>
+    public class IgnoreScheduleRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
new file mode 100644
index 0000000000..af73352bcc
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+    /// <summary>
+    /// Local access handler.
+    /// </summary>
+    public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public LocalAccessHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, localAccessOnly: true);
+            if (!validated)
+            {
+                context.Fail();
+            }
+            else
+            {
+                context.Succeed(requirement);
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
new file mode 100644
index 0000000000..761127fa40
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+    /// <summary>
+    /// The local access authorization requirement.
+    /// </summary>
+    public class LocalAccessRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
index 2d3bb1aa48..b235c4b63b 100644
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
@@ -1,21 +1,43 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Auth.RequiresElevationPolicy
 {
     /// <summary>
     /// Authorization handler for requiring elevated privileges.
     /// </summary>
-    public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
+    public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public RequiresElevationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
         /// <inheritdoc />
         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
         {
-            if (context.User.IsInRole(UserRoles.Administrator))
+            var validated = ValidateClaims(context.User);
+            if (validated && context.User.IsInRole(UserRoles.Administrator))
             {
                 context.Succeed(requirement);
             }
+            else
+            {
+                context.Fail();
+            }
 
             return Task.CompletedTask;
         }
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
new file mode 100644
index 0000000000..4d7c7135d5
--- /dev/null
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -0,0 +1,38 @@
+namespace Jellyfin.Api.Constants
+{
+    /// <summary>
+    /// Internal claim types for authorization.
+    /// </summary>
+    public static class InternalClaimTypes
+    {
+        /// <summary>
+        /// User Id.
+        /// </summary>
+        public const string UserId = "Jellyfin-UserId";
+
+        /// <summary>
+        /// Device Id.
+        /// </summary>
+        public const string DeviceId = "Jellyfin-DeviceId";
+
+        /// <summary>
+        /// Device.
+        /// </summary>
+        public const string Device = "Jellyfin-Device";
+
+        /// <summary>
+        /// Client.
+        /// </summary>
+        public const string Client = "Jellyfin-Client";
+
+        /// <summary>
+        /// Version.
+        /// </summary>
+        public const string Version = "Jellyfin-Version";
+
+        /// <summary>
+        /// Token.
+        /// </summary>
+        public const string Token = "Jellyfin-Token";
+    }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index e2b383f75d..cf574e43df 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants
     /// </summary>
     public static class Policies
     {
+        /// <summary>
+        /// Policy name for default authorization.
+        /// </summary>
+        public const string DefaultAuthorization = "DefaultAuthorization";
+
         /// <summary>
         /// Policy name for requiring first time setup or elevated privileges.
         /// </summary>
@@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
         /// Policy name for requiring elevated privileges.
         /// </summary>
         public const string RequiresElevation = "RequiresElevation";
+
+        /// <summary>
+        /// Policy name for allowing local access only.
+        /// </summary>
+        public const string LocalAccessOnly = "LocalAccessOnly";
+
+        /// <summary>
+        /// Policy name for escaping schedule controls.
+        /// </summary>
+        public const string IgnoreSchedule = "IgnoreSchedule";
     }
 }
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
new file mode 100644
index 0000000000..a07d4ed820
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -0,0 +1,77 @@
+#nullable enable
+
+using System;
+using System.Linq;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Claim Helpers.
+    /// </summary>
+    public static class ClaimHelpers
+    {
+        /// <summary>
+        /// Get user id from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>User id.</returns>
+        public static Guid? GetUserId(in ClaimsPrincipal user)
+        {
+            var value = GetClaimValue(user, InternalClaimTypes.UserId);
+            return string.IsNullOrEmpty(value)
+                ? null
+                : (Guid?)Guid.Parse(value);
+        }
+
+        /// <summary>
+        /// Get device id from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Device id.</returns>
+        public static string? GetDeviceId(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.DeviceId);
+
+        /// <summary>
+        /// Get device from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Device.</returns>
+        public static string? GetDevice(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Device);
+
+        /// <summary>
+        /// Get client from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Client.</returns>
+        public static string? GetClient(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Client);
+
+        /// <summary>
+        /// Get version from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Version.</returns>
+        public static string? GetVersion(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Version);
+
+        /// <summary>
+        /// Get token from claims.
+        /// </summary>
+        /// <param name="user">Current claims principal.</param>
+        /// <returns>Token.</returns>
+        public static string? GetToken(in ClaimsPrincipal user)
+            => GetClaimValue(user, InternalClaimTypes.Token);
+
+        private static string? GetClaimValue(in ClaimsPrincipal user, string name)
+        {
+            return user?.Identities
+                .SelectMany(c => c.Claims)
+                .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
+                .Select(claim => claim.Value)
+                .FirstOrDefault();
+        }
+    }
+}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 9cdaa0eb16..1ec77d716c 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -5,7 +5,10 @@ using System.Linq;
 using System.Reflection;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
@@ -33,16 +36,19 @@ namespace Jellyfin.Server.Extensions
         /// <returns>The updated service collection.</returns>
         public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
         {
+            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
             return serviceCollection.AddAuthorizationCore(options =>
             {
                 options.AddPolicy(
-                    Policies.RequiresElevation,
+                    Policies.DefaultAuthorization,
                     policy =>
                     {
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
-                        policy.AddRequirements(new RequiresElevationRequirement());
+                        policy.AddRequirements(new DefaultAuthorizationRequirement());
                     });
                 options.AddPolicy(
                     Policies.FirstTimeSetupOrElevated,
@@ -51,6 +57,27 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
                     });
+                options.AddPolicy(
+                    Policies.IgnoreSchedule,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new IgnoreScheduleRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.LocalAccessOnly,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LocalAccessRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.RequiresElevation,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new RequiresElevationRequirement());
+                    });
             });
         }
 
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index d8f6d19da0..2055a656a7 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
+    /// <summary>
+    /// IAuthService.
+    /// </summary>
     public interface IAuthService
     {
-        void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
+        /// <summary>
+        /// Authenticate and authorize request.
+        /// </summary>
+        /// <param name="request">Request.</param>
+        /// <param name="authAttribtutes">Authorization attributes.</param>
+        void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
 
-        User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
+        /// <summary>
+        /// Authenticate and authorize request.
+        /// </summary>
+        /// <param name="request">Request.</param>
+        /// <param name="authAttribtutes">Authorization attributes.</param>
+        /// <returns>Authenticated user.</returns>
+        User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
+
+        /// <summary>
+        /// Authenticate request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Authorization information. Null if unauthenticated.</returns>
+        AuthorizationInfo Authenticate(HttpRequest request);
     }
 }

From a8adbef74fc8300190c463a9c585b55dcfb0c78e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 15 Jun 2020 13:21:18 -0600
Subject: [PATCH 187/463] Add GetAuthorizationInfo for netcore HttpRequest

---
 .../Security/AuthorizationContext.cs          | 132 ++++++++++++------
 .../Net/IAuthorizationContext.cs              |  11 ++
 2 files changed, 104 insertions(+), 39 deletions(-)

diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 9558cb4c66..deb9b5ebb0 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 using Microsoft.Net.Http.Headers;
 
 namespace Emby.Server.Implementations.HttpServer.Security
@@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetAuthorization(requestContext);
         }
 
+        public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+        {
+            var auth = GetAuthorizationDictionary(requestContext);
+            var (authInfo, _) =
+                GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+            return authInfo;
+        }
+
         /// <summary>
         /// Gets the authorization.
         /// </summary>
@@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private AuthorizationInfo GetAuthorization(IRequest httpReq)
         {
             var auth = GetAuthorizationDictionary(httpReq);
+            var (authInfo, originalAuthInfo) =
+                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+
+            if (originalAuthInfo != null)
+            {
+                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+            }
 
+            httpReq.Items["AuthorizationInfo"] = authInfo;
+            return authInfo;
+        }
+
+        private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+            in Dictionary<string, string> auth,
+            in IHeaderDictionary headers,
+            in IQueryCollection queryString)
+        {
             string deviceId = null;
             string device = null;
             string client = null;
@@ -64,19 +89,31 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-Emby-Token"];
+                token = headers["X-Jellyfin-Token"];
+            }
+
+            if (string.IsNullOrEmpty(token))
+            {
+                token = headers["X-Emby-Token"];
             }
 
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.Headers["X-MediaBrowser-Token"];
+                token = headers["X-MediaBrowser-Token"];
             }
+
             if (string.IsNullOrEmpty(token))
             {
-                token = httpReq.QueryString["api_key"];
+                token = queryString["ApiKey"];
             }
 
-            var info = new AuthorizationInfo
+            // TODO depricate this query parameter.
+            if (string.IsNullOrEmpty(token))
+            {
+                token = queryString["api_key"];
+            }
+
+            var authInfo = new AuthorizationInfo
             {
                 Client = client,
                 Device = device,
@@ -85,6 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 Token = token
             };
 
+            AuthenticationInfo originalAuthenticationInfo = null;
             if (!string.IsNullOrWhiteSpace(token))
             {
                 var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +130,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
                     AccessToken = token
                 });
 
-                var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+                originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 
-                if (tokenInfo != null)
+                if (originalAuthenticationInfo != null)
                 {
                     var updateToken = false;
 
                     // TODO: Remove these checks for IsNullOrWhiteSpace
-                    if (string.IsNullOrWhiteSpace(info.Client))
+                    if (string.IsNullOrWhiteSpace(authInfo.Client))
                     {
-                        info.Client = tokenInfo.AppName;
+                        authInfo.Client = originalAuthenticationInfo.AppName;
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.DeviceId))
+                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
                     {
-                        info.DeviceId = tokenInfo.DeviceId;
+                        authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
                     }
 
                     // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                    var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+                    var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
 
-                    if (string.IsNullOrWhiteSpace(info.Device))
+                    if (string.IsNullOrWhiteSpace(authInfo.Device))
                     {
-                        info.Device = tokenInfo.DeviceName;
+                        authInfo.Device = originalAuthenticationInfo.DeviceName;
                     }
-
-                    else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.DeviceName = info.Device;
+                            originalAuthenticationInfo.DeviceName = authInfo.Device;
                         }
                     }
 
-                    if (string.IsNullOrWhiteSpace(info.Version))
+                    if (string.IsNullOrWhiteSpace(authInfo.Version))
                     {
-                        info.Version = tokenInfo.AppVersion;
+                        authInfo.Version = originalAuthenticationInfo.AppVersion;
                     }
-                    else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+                    else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
                     {
                         if (allowTokenInfoUpdate)
                         {
                             updateToken = true;
-                            tokenInfo.AppVersion = info.Version;
+                            originalAuthenticationInfo.AppVersion = authInfo.Version;
                         }
                     }
 
-                    if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+                    if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
                     {
-                        tokenInfo.DateLastActivity = DateTime.UtcNow;
+                        originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
                         updateToken = true;
                     }
 
-                    if (!tokenInfo.UserId.Equals(Guid.Empty))
+                    if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
                     {
-                        info.User = _userManager.GetUserById(tokenInfo.UserId);
+                        authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
 
-                        if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+                        if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
                         {
-                            tokenInfo.UserName = info.User.Username;
+                            originalAuthenticationInfo.UserName = authInfo.User.Username;
                             updateToken = true;
                         }
                     }
 
                     if (updateToken)
                     {
-                        _authRepo.Update(tokenInfo);
+                        _authRepo.Update(originalAuthenticationInfo);
                     }
                 }
-                httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
             }
 
-            httpReq.Items["AuthorizationInfo"] = info;
-
-            return info;
+            return (authInfo, originalAuthenticationInfo);
         }
 
         /// <summary>
@@ -176,7 +210,32 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
         {
-            var auth = httpReq.Headers["X-Emby-Authorization"];
+            var auth = httpReq.Headers["X-Jellyfin-Authorization"];
+            if (string.IsNullOrEmpty(auth))
+            {
+                auth = httpReq.Headers["X-Emby-Authorization"];
+            }
+
+            if (string.IsNullOrEmpty(auth))
+            {
+                auth = httpReq.Headers[HeaderNames.Authorization];
+            }
+
+            return GetAuthorization(auth);
+        }
+
+        /// <summary>
+        /// Gets the auth.
+        /// </summary>
+        /// <param name="httpReq">The HTTP req.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+        {
+            var auth = httpReq.Headers["X-Jellyfin-Authorization"];
+            if (string.IsNullOrEmpty(auth))
+            {
+                auth = httpReq.Headers["X-Emby-Authorization"];
+            }
 
             if (string.IsNullOrEmpty(auth))
             {
@@ -206,7 +265,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 return null;
             }
 
-            var acceptedNames = new[] { "MediaBrowser", "Emby" };
+            var acceptedNames = new[] { "MediaBrowser", "Emby", "Jellyfin" };
 
             // It has to be a digest request
             if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
@@ -236,12 +295,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
         private static string NormalizeValue(string value)
         {
-            if (string.IsNullOrEmpty(value))
-            {
-                return value;
-            }
-
-            return WebUtility.HtmlEncode(value);
+            return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
         }
     }
 }
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 61598391ff..37a7425b9d 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,7 +1,11 @@
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
+    /// <summary>
+    /// IAuthorization context.
+    /// </summary>
     public interface IAuthorizationContext
     {
         /// <summary>
@@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
         /// <param name="requestContext">The request context.</param>
         /// <returns>AuthorizationInfo.</returns>
         AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+
+        /// <summary>
+        /// Gets the authorization information.
+        /// </summary>
+        /// <param name="requestContext">The request context.</param>
+        /// <returns>AuthorizationInfo.</returns>
+        AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
     }
 }

From a952d1567078dae0f5c732063e14a161cd784c0c Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 16 Jun 2020 16:08:31 +0200
Subject: [PATCH 188/463] Await Task from _libraryManager

---
 Jellyfin.Api/Controllers/LibraryStructureController.cs | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index ecbfed4693..a989efe7f1 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CA1801
 
 using System;
@@ -73,7 +74,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult AddVirtualFolder(
+        public async Task<ActionResult> AddVirtualFolder(
             [FromQuery] string name,
             [FromQuery] string collectionType,
             [FromQuery] bool refreshLibrary,
@@ -87,7 +88,7 @@ namespace Jellyfin.Api.Controllers
                 libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
             }
 
-            _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary);
+            await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
 
             return NoContent();
         }
@@ -101,11 +102,11 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RemoveVirtualFolder(
+        public async Task<ActionResult> RemoveVirtualFolder(
             [FromQuery] string name,
             [FromQuery] bool refreshLibrary)
         {
-            _libraryManager.RemoveVirtualFolder(name, refreshLibrary);
+            await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
             return NoContent();
         }
 

From 774fdbd031f96dada757470c6e935f0667c775f1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 16 Jun 2020 14:12:40 -0600
Subject: [PATCH 189/463] Fix tests.

---
 .../Auth/CustomAuthenticationHandler.cs       |  2 +-
 .../FirstTimeSetupOrElevatedHandler.cs        |  1 +
 .../Auth/CustomAuthenticationHandlerTests.cs  | 71 +++++------------
 .../FirstTimeSetupOrElevatedHandlerTests.cs   | 66 ++++++++++++++--
 .../RequiresElevationHandlerTests.cs          | 78 +++++++++++++++++--
 .../Jellyfin.Api.Tests.csproj                 |  1 +
 6 files changed, 155 insertions(+), 64 deletions(-)

diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index d4d40da577..5e5e25e847 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Auth
                     new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
                     new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
                     new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
-                    new Claim(InternalClaimTypes.Token, authorizationInfo.Token)
+                    new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
                 };
 
                 var identity = new ClaimsIdentity(claims, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index 0b12f7d3c2..decbe0c035 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -38,6 +38,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
             {
                 context.Succeed(firstTimeSetupOrElevatedRequirement);
+                return Task.CompletedTask;
             }
 
             var validated = ValidateClaims(context.User);
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 362d41b015..4ea5094b66 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Linq;
 using System.Security.Claims;
-using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
@@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Http;
@@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
         private readonly IFixture _fixture;
 
         private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
-        private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
-        private readonly Mock<ISystemClock> _clockMock;
-        private readonly Mock<IServiceProvider> _serviceProviderMock;
-        private readonly Mock<IAuthenticationService> _authenticationServiceMock;
-        private readonly UrlEncoder _urlEncoder;
-        private readonly HttpContext _context;
 
         private readonly CustomAuthenticationHandler _sut;
         private readonly AuthenticationScheme _scheme;
@@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
             AllowFixtureCircularDependencies();
 
             _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
-            _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
-            _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
-            _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
-            _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+            var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+            var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+            var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
             _fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
 
-            _urlEncoder = UrlEncoder.Default;
+            serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+                .Returns(authenticationServiceMock.Object);
 
-            _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
-                .Returns(_authenticationServiceMock.Object);
-
-            _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+            optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
                 .Returns(new AuthenticationSchemeOptions
                 {
                     ForwardAuthenticate = null
                 });
 
-            _context = new DefaultHttpContext
+            HttpContext context = new DefaultHttpContext
             {
-                RequestServices = _serviceProviderMock.Object
+                RequestServices = serviceProviderMock.Object
             };
 
             _scheme = new AuthenticationScheme(
@@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
                 typeof(CustomAuthenticationHandler));
 
             _sut = _fixture.Create<CustomAuthenticationHandler>();
-            _sut.InitializeAsync(_scheme, _context).Wait();
-        }
-
-        [Fact]
-        public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
-        {
-            _jellyfinAuthServiceMock.Setup(
-                    a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
-                .Returns((User?)null);
-
-            var authenticateResult = await _sut.AuthenticateAsync();
-
-            Assert.False(authenticateResult.Succeeded);
-            Assert.True(authenticateResult.None);
-            // TODO return when legacy API is removed.
-            // Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+            _sut.InitializeAsync(_scheme, context).Wait();
         }
 
         [Fact]
@@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
 
             _jellyfinAuthServiceMock.Setup(
                     a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
+                        It.IsAny<HttpRequest>()))
                 .Throws(new SecurityException(errorMessage));
 
             var authenticateResult = await _sut.AuthenticateAsync();
@@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
         [Fact]
         public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
         {
-            var user = SetupUser();
+            var authorizationInfo = SetupUser();
             var authenticateResult = await _sut.AuthenticateAsync();
 
-            Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
+            Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
         }
 
         [Theory]
@@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
         [InlineData(false)]
         public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
         {
-            var user = SetupUser(isAdmin);
+            var authorizationInfo = SetupUser(isAdmin);
             var authenticateResult = await _sut.AuthenticateAsync();
 
-            var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
+            var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
             Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
         }
 
@@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
             Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
         }
 
-        private User SetupUser(bool isAdmin = false)
+        private AuthorizationInfo SetupUser(bool isAdmin = false)
         {
-            var user = _fixture.Create<User>();
-            user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+            var authorizationInfo = _fixture.Create<AuthorizationInfo>();
+            authorizationInfo.User = _fixture.Create<User>();
+            authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
 
             _jellyfinAuthServiceMock.Setup(
                     a => a.Authenticate(
-                        It.IsAny<HttpRequest>(),
-                        It.IsAny<AuthenticatedAttribute>()))
-                .Returns(user);
+                        It.IsAny<HttpRequest>()))
+                .Returns(authorizationInfo);
 
-            return user;
+            return authorizationInfo;
         }
 
         private void AllowFixtureCircularDependencies()
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
index e40af703f9..e455df6435 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -1,13 +1,21 @@
+using System;
 using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
 using System.Security.Claims;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Moq;
 using Xunit;
 
@@ -15,15 +23,28 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
 {
     public class FirstTimeSetupOrElevatedHandlerTests
     {
+        /// <summary>
+        /// 127.0.0.1.
+        /// </summary>
+        private const long InternalIp = 16777343;
+
+        /// <summary>
+        /// 1.1.1.1.
+        /// </summary>
+        /// private const long ExternalIp = 16843009;
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
         private readonly FirstTimeSetupOrElevatedHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
         public FirstTimeSetupOrElevatedHandlerTests()
         {
             var fixture = new Fixture().Customize(new AutoMoqCustomization());
             _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
             _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
 
             _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
         }
@@ -35,8 +56,15 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
         {
             SetupConfigurationManager(false);
-            var user = SetupUser(userRole);
-            var context = new AuthorizationHandlerContext(_requirements, user, null);
+            var (user, claims) = SetupUser(userRole);
+
+            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
+                .Returns(user);
+
+            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+                .Returns(new IPAddress(InternalIp));
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.True(context.HasSucceeded);
@@ -49,18 +77,42 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
         {
             SetupConfigurationManager(true);
-            var user = SetupUser(userRole);
-            var context = new AuthorizationHandlerContext(_requirements, user, null);
+            var (user, claims) = SetupUser(userRole);
+
+            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
+                .Returns(user);
+
+            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+                .Returns(new IPAddress(InternalIp));
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);
         }
 
-        private static ClaimsPrincipal SetupUser(string role)
+        private static (User, ClaimsPrincipal) SetupUser(string role)
         {
-            var claims = new[] { new Claim(ClaimTypes.Role, role) };
+            var user = new User(
+                "jellyfin",
+                typeof(DefaultAuthenticationProvider).FullName,
+                typeof(DefaultPasswordResetProvider).FullName);
+
+            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
+            var claims = new[]
+            {
+                new Claim(ClaimTypes.Role, role),
+                new Claim(ClaimTypes.Name, "jellyfin"),
+                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.Device, "test"),
+                new Claim(InternalClaimTypes.Client, "test"),
+                new Claim(InternalClaimTypes.Version, "test"),
+                new Claim(InternalClaimTypes.Token, "test"),
+            };
+
             var identity = new ClaimsIdentity(claims);
-            return new ClaimsPrincipal(identity);
+            return (user, new ClaimsPrincipal(identity));
         }
 
         private void SetupConfigurationManager(bool startupWizardCompleted)
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
index cd05a8328d..58eae84789 100644
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -1,20 +1,48 @@
+using System;
 using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
 using System.Security.Claims;
 using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
 using Xunit;
 
 namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
 {
     public class RequiresElevationHandlerTests
     {
+        /// <summary>
+        /// 127.0.0.1.
+        /// </summary>
+        private const long InternalIp = 16777343;
+
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
         private readonly RequiresElevationHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
         public RequiresElevationHandlerTests()
         {
-            _sut = new RequiresElevationHandler();
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<RequiresElevationHandler>();
         }
 
         [Theory]
@@ -23,16 +51,54 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
         [InlineData(UserRoles.Guest, false)]
         public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
         {
-            var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
+            SetupConfigurationManager(true);
+            var (user, claims) = SetupUser(role);
 
-            var claims = new[] { new Claim(ClaimTypes.Role, role) };
-            var identity = new ClaimsIdentity(claims);
-            var user = new ClaimsPrincipal(identity);
+            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
+                .Returns(user);
+
+            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+                .Returns(new IPAddress(InternalIp));
 
-            var context = new AuthorizationHandlerContext(requirements, user, null);
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);
         }
+
+        private static (User, ClaimsPrincipal) SetupUser(string role)
+        {
+            var user = new User(
+                "jellyfin",
+                typeof(DefaultAuthenticationProvider).FullName,
+                typeof(DefaultPasswordResetProvider).FullName);
+
+            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
+            var claims = new[]
+            {
+                new Claim(ClaimTypes.Role, role),
+                new Claim(ClaimTypes.Name, "jellyfin"),
+                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.Device, "test"),
+                new Claim(InternalClaimTypes.Client, "test"),
+                new Claim(InternalClaimTypes.Version, "test"),
+                new Claim(InternalClaimTypes.Token, "test"),
+            };
+
+            var identity = new ClaimsIdentity(claims);
+            return (user, new ClaimsPrincipal(identity));
+        }
+
+        private void SetupConfigurationManager(bool startupWizardCompleted)
+        {
+            var commonConfiguration = new BaseApplicationConfiguration
+            {
+                IsStartupWizardCompleted = startupWizardCompleted
+            };
+
+            _configurationManagerMock.Setup(c => c.CommonConfiguration)
+                .Returns(commonConfiguration);
+        }
     }
 }
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index aedcc7c42e..010fad520a 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -35,6 +35,7 @@
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
     <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
+    <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
   </ItemGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

From c24666253c48ef17402bd8ddb7688821616ec6ba Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 16 Jun 2020 14:15:58 -0600
Subject: [PATCH 190/463] Add Default authorization policy

---
 Jellyfin.Api/Controllers/ConfigurationController.cs    | 2 +-
 Jellyfin.Api/Controllers/DevicesController.cs          | 2 +-
 Jellyfin.Api/Controllers/PackageController.cs          | 2 +-
 Jellyfin.Api/Controllers/SearchController.cs           | 3 ++-
 Jellyfin.Api/Controllers/SubtitleController.cs         | 8 ++++----
 Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 3 ++-
 6 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 780a38aa81..5d37c9ade9 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// Configuration Controller.
     /// </summary>
     [Route("System")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ConfigurationController : BaseJellyfinApiController
     {
         private readonly IServerConfigurationManager _configurationManager;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 1754b0cbda..2f53628514 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Devices Controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DevicesController : BaseJellyfinApiController
     {
         private readonly IDeviceManager _deviceManager;
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 8200f891c8..b5b21fdeed 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// Package Controller.
     /// </summary>
     [Route("Packages")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PackageController : BaseJellyfinApiController
     {
         private readonly IInstallationManager _installationManager;
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index ec05e4fb4f..d971889db8 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
     /// Search controller.
     /// </summary>
     [Route("/Search/Hints")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class SearchController : BaseJellyfinApiController
     {
         private readonly ISearchEngine _searchEngine;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 97df8c60d8..9aff359967 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Subtitles retrieved.</response>
         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
         [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid id,
@@ -130,7 +130,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Subtitle downloaded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid id,
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
         [HttpGet("/Providers/Subtitles/Subtitles/{id}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
         public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
@@ -250,7 +250,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Subtitle playlist retrieved.</response>
         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
         [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitlePlaylist(
             [FromRoute] Guid id,
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 86d9322fe4..32e26ff0be 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -4,6 +4,7 @@ using System;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
@@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class VideoAttachmentsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;

From b22fdbf59ee6536ca255ca3c57a13e5b9293fd78 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 16 Jun 2020 16:42:10 -0600
Subject: [PATCH 191/463] Add tests and cleanup

---
 .../DefaultAuthorizationHandlerTests.cs       | 54 +++++++++++
 .../FirstTimeSetupOrElevatedHandlerTests.cs   | 80 +++-------------
 .../IgnoreScheduleHandlerTests.cs             | 60 ++++++++++++
 .../RequiresElevationHandlerTests.cs          | 62 ++-----------
 tests/Jellyfin.Api.Tests/TestHelpers.cs       | 92 +++++++++++++++++++
 5 files changed, 224 insertions(+), 124 deletions(-)
 create mode 100644 tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
 create mode 100644 tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
 create mode 100644 tests/Jellyfin.Api.Tests/TestHelpers.cs

diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
new file mode 100644
index 0000000000..991ea3262f
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
+{
+    public class DefaultAuthorizationHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly DefaultAuthorizationHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+        public DefaultAuthorizationHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<DefaultAuthorizationHandler>();
+        }
+
+        [Theory]
+        [InlineData(UserRoles.Administrator)]
+        [InlineData(UserRoles.Guest)]
+        [InlineData(UserRoles.User)]
+        public async Task ShouldSucceedOnUser(string userRole)
+        {
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var (_, claims) = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole,
+                TestHelpers.InternalIp);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+            await _sut.HandleAsync(context);
+            Assert.True(context.HasSucceeded);
+        }
+    }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
index e455df6435..2b49419082 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -1,19 +1,11 @@
-using System;
 using System.Collections.Generic;
-using System.Globalization;
-using System.Net;
-using System.Security.Claims;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Moq;
@@ -23,15 +15,6 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
 {
     public class FirstTimeSetupOrElevatedHandlerTests
     {
-        /// <summary>
-        /// 127.0.0.1.
-        /// </summary>
-        private const long InternalIp = 16777343;
-
-        /// <summary>
-        /// 1.1.1.1.
-        /// </summary>
-        /// private const long ExternalIp = 16843009;
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
         private readonly FirstTimeSetupOrElevatedHandler _sut;
@@ -55,14 +38,12 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         [InlineData(UserRoles.User)]
         public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
         {
-            SetupConfigurationManager(false);
-            var (user, claims) = SetupUser(userRole);
-
-            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
-                .Returns(user);
-
-            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
-                .Returns(new IPAddress(InternalIp));
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
+            var (_, claims) = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole,
+                TestHelpers.InternalIp);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
@@ -76,54 +57,17 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         [InlineData(UserRoles.User, false)]
         public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
         {
-            SetupConfigurationManager(true);
-            var (user, claims) = SetupUser(userRole);
-
-            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
-                .Returns(user);
-
-            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
-                .Returns(new IPAddress(InternalIp));
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var (_, claims) = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                userRole,
+                TestHelpers.InternalIp);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);
         }
-
-        private static (User, ClaimsPrincipal) SetupUser(string role)
-        {
-            var user = new User(
-                "jellyfin",
-                typeof(DefaultAuthenticationProvider).FullName,
-                typeof(DefaultPasswordResetProvider).FullName);
-
-            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
-            var claims = new[]
-            {
-                new Claim(ClaimTypes.Role, role),
-                new Claim(ClaimTypes.Name, "jellyfin"),
-                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
-                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
-                new Claim(InternalClaimTypes.Device, "test"),
-                new Claim(InternalClaimTypes.Client, "test"),
-                new Claim(InternalClaimTypes.Version, "test"),
-                new Claim(InternalClaimTypes.Token, "test"),
-            };
-
-            var identity = new ClaimsIdentity(claims);
-            return (user, new ClaimsPrincipal(identity));
-        }
-
-        private void SetupConfigurationManager(bool startupWizardCompleted)
-        {
-            var commonConfiguration = new BaseApplicationConfiguration
-            {
-                IsStartupWizardCompleted = startupWizardCompleted
-            };
-
-            _configurationManagerMock.Setup(c => c.CommonConfiguration)
-                .Returns(commonConfiguration);
-        }
     }
 }
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
new file mode 100644
index 0000000000..25acfb581f
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
+{
+    public class IgnoreScheduleHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly IgnoreScheduleHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+
+        private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
+
+        public IgnoreScheduleHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+            _sut = fixture.Create<IgnoreScheduleHandler>();
+        }
+
+        [Theory]
+        [InlineData(UserRoles.Administrator, true)]
+        [InlineData(UserRoles.User, true)]
+        [InlineData(UserRoles.Guest, true)]
+        public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
+        {
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var (_, claims) = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                role,
+                TestHelpers.InternalIp,
+                _accessSchedules);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+            await _sut.HandleAsync(context);
+            Assert.Equal(shouldSucceed, context.HasSucceeded);
+        }
+    }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
index 58eae84789..f4617d0a42 100644
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -1,19 +1,11 @@
-using System;
 using System.Collections.Generic;
-using System.Globalization;
-using System.Net;
-using System.Security.Claims;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Moq;
@@ -23,11 +15,6 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
 {
     public class RequiresElevationHandlerTests
     {
-        /// <summary>
-        /// 127.0.0.1.
-        /// </summary>
-        private const long InternalIp = 16777343;
-
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
         private readonly RequiresElevationHandler _sut;
@@ -51,54 +38,17 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
         [InlineData(UserRoles.Guest, false)]
         public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
         {
-            SetupConfigurationManager(true);
-            var (user, claims) = SetupUser(role);
-
-            _userManagerMock.Setup(u => u.GetUserById(It.IsAny<Guid>()))
-                .Returns(user);
-
-            _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
-                .Returns(new IPAddress(InternalIp));
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var (_, claims) = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                role,
+                TestHelpers.InternalIp);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
             await _sut.HandleAsync(context);
             Assert.Equal(shouldSucceed, context.HasSucceeded);
         }
-
-        private static (User, ClaimsPrincipal) SetupUser(string role)
-        {
-            var user = new User(
-                "jellyfin",
-                typeof(DefaultAuthenticationProvider).FullName,
-                typeof(DefaultPasswordResetProvider).FullName);
-
-            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
-            var claims = new[]
-            {
-                new Claim(ClaimTypes.Role, role),
-                new Claim(ClaimTypes.Name, "jellyfin"),
-                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
-                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
-                new Claim(InternalClaimTypes.Device, "test"),
-                new Claim(InternalClaimTypes.Client, "test"),
-                new Claim(InternalClaimTypes.Version, "test"),
-                new Claim(InternalClaimTypes.Token, "test"),
-            };
-
-            var identity = new ClaimsIdentity(claims);
-            return (user, new ClaimsPrincipal(identity));
-        }
-
-        private void SetupConfigurationManager(bool startupWizardCompleted)
-        {
-            var commonConfiguration = new BaseApplicationConfiguration
-            {
-                IsStartupWizardCompleted = startupWizardCompleted
-            };
-
-            _configurationManagerMock.Setup(c => c.CommonConfiguration)
-                .Returns(commonConfiguration);
-        }
     }
 }
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
new file mode 100644
index 0000000000..4617486fd9
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
+
+namespace Jellyfin.Api.Tests
+{
+    public static class TestHelpers
+    {
+        /// <summary>
+        /// 127.0.0.1.
+        /// </summary>
+        public const long InternalIp = 16777343;
+
+        /// <summary>
+        /// 1.1.1.1.
+        /// </summary>
+        public const long ExternalIp = 16843009;
+
+        public static (User, ClaimsPrincipal) SetupUser(
+            Mock<IUserManager> userManagerMock,
+            Mock<IHttpContextAccessor> httpContextAccessorMock,
+            string role,
+            long ip,
+            IEnumerable<AccessSchedule>? accessSchedules = null)
+        {
+            var user = new User(
+                "jellyfin",
+                typeof(DefaultAuthenticationProvider).FullName,
+                typeof(DefaultPasswordResetProvider).FullName);
+
+            // Set administrator flag.
+            user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
+
+            // Add access schedules if set.
+            if (accessSchedules != null)
+            {
+                foreach (var accessSchedule in accessSchedules)
+                {
+                    user.AccessSchedules.Add(accessSchedule);
+                }
+            }
+
+            var claims = new[]
+            {
+                new Claim(ClaimTypes.Role, role),
+                new Claim(ClaimTypes.Name, "jellyfin"),
+                new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
+                new Claim(InternalClaimTypes.Device, "test"),
+                new Claim(InternalClaimTypes.Client, "test"),
+                new Claim(InternalClaimTypes.Version, "test"),
+                new Claim(InternalClaimTypes.Token, "test"),
+            };
+
+            var identity = new ClaimsIdentity(claims);
+
+            userManagerMock
+                .Setup(u => u.GetUserById(It.IsAny<Guid>()))
+                .Returns(user);
+
+            httpContextAccessorMock
+                .Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+                .Returns(new IPAddress(ip));
+
+            return (user, new ClaimsPrincipal(identity));
+        }
+
+        public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)
+        {
+            var commonConfiguration = new BaseApplicationConfiguration
+            {
+                IsStartupWizardCompleted = startupWizardCompleted
+            };
+
+            configurationManagerMock
+                .Setup(c => c.CommonConfiguration)
+                .Returns(commonConfiguration);
+        }
+    }
+}

From b451eb0bdc1594c88af11ae807fb7f3b3c4ef124 Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Tue, 16 Jun 2020 16:45:17 -0600
Subject: [PATCH 192/463] Update
 Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

Co-authored-by: Patrick Barron <18354464+barronpm@users.noreply.github.com>
---
 .../HttpServer/Security/AuthorizationContext.cs                 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index deb9b5ebb0..b9fca67bf0 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 token = queryString["ApiKey"];
             }
 
-            // TODO depricate this query parameter.
+            // TODO deprecate this query parameter.
             if (string.IsNullOrEmpty(token))
             {
                 token = queryString["api_key"];

From 29917699f0854f504452e62ee7be4bff0a4a206d Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 16 Jun 2020 16:55:02 -0600
Subject: [PATCH 193/463] Further cleanup and add final tests

---
 .../DefaultAuthorizationHandlerTests.cs       |  5 +-
 .../FirstTimeSetupOrElevatedHandlerTests.cs   | 10 ++--
 .../IgnoreScheduleHandlerTests.cs             |  6 +-
 .../LocalAccessHandlerTests.cs                | 58 +++++++++++++++++++
 .../RequiresElevationHandlerTests.cs          |  5 +-
 tests/Jellyfin.Api.Tests/TestHelpers.cs       | 17 +-----
 6 files changed, 73 insertions(+), 28 deletions(-)
 create mode 100644 tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs

diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
index 991ea3262f..a62fd8d5ae 100644
--- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
         public async Task ShouldSucceedOnUser(string userRole)
         {
             TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
-            var (_, claims) = TestHelpers.SetupUser(
+            var claims = TestHelpers.SetupUser(
                 _userManagerMock,
                 _httpContextAccessor,
-                userRole,
-                TestHelpers.InternalIp);
+                userRole);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
index 2b49419082..ee42216e46 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
         {
             TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
-            var (_, claims) = TestHelpers.SetupUser(
+            var claims = TestHelpers.SetupUser(
                 _userManagerMock,
                 _httpContextAccessor,
-                userRole,
-                TestHelpers.InternalIp);
+                userRole);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
@@ -58,11 +57,10 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
         public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
         {
             TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
-            var (_, claims) = TestHelpers.SetupUser(
+            var claims = TestHelpers.SetupUser(
                 _userManagerMock,
                 _httpContextAccessor,
-                userRole,
-                TestHelpers.InternalIp);
+                userRole);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
index 25acfb581f..b65d45aa08 100644
--- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -24,6 +24,9 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
         private readonly Mock<IUserManager> _userManagerMock;
         private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
+        /// <summary>
+        /// Globally disallow access.
+        /// </summary>
         private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
 
         public IgnoreScheduleHandlerTests()
@@ -44,11 +47,10 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
         public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
         {
             TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
-            var (_, claims) = TestHelpers.SetupUser(
+            var claims = TestHelpers.SetupUser(
                 _userManagerMock,
                 _httpContextAccessor,
                 role,
-                TestHelpers.InternalIp,
                 _accessSchedules);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
new file mode 100644
index 0000000000..09ffa84689
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
+{
+    public class LocalAccessHandlerTests
+    {
+        private readonly Mock<IConfigurationManager> _configurationManagerMock;
+        private readonly List<IAuthorizationRequirement> _requirements;
+        private readonly LocalAccessHandler _sut;
+        private readonly Mock<IUserManager> _userManagerMock;
+        private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
+        private readonly Mock<INetworkManager> _networkManagerMock;
+
+        public LocalAccessHandlerTests()
+        {
+            var fixture = new Fixture().Customize(new AutoMoqCustomization());
+            _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+            _requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
+            _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+            _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+            _networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
+
+            _sut = fixture.Create<LocalAccessHandler>();
+        }
+
+        [Theory]
+        [InlineData(true, true)]
+        [InlineData(false, false)]
+        public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
+        {
+            _networkManagerMock
+                .Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
+                .Returns(isInLocalNetwork);
+
+            TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+            var claims = TestHelpers.SetupUser(
+                _userManagerMock,
+                _httpContextAccessor,
+                UserRoles.User);
+
+            var context = new AuthorizationHandlerContext(_requirements, claims, null);
+            await _sut.HandleAsync(context);
+            Assert.Equal(shouldSucceed, context.HasSucceeded);
+        }
+    }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
index f4617d0a42..ffe88fcdeb 100644
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -39,11 +39,10 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
         public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
         {
             TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
-            var (_, claims) = TestHelpers.SetupUser(
+            var claims = TestHelpers.SetupUser(
                 _userManagerMock,
                 _httpContextAccessor,
-                role,
-                TestHelpers.InternalIp);
+                role);
 
             var context = new AuthorizationHandlerContext(_requirements, claims, null);
 
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
index 4617486fd9..a4dd4e4092 100644
--- a/tests/Jellyfin.Api.Tests/TestHelpers.cs
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -18,21 +18,10 @@ namespace Jellyfin.Api.Tests
 {
     public static class TestHelpers
     {
-        /// <summary>
-        /// 127.0.0.1.
-        /// </summary>
-        public const long InternalIp = 16777343;
-
-        /// <summary>
-        /// 1.1.1.1.
-        /// </summary>
-        public const long ExternalIp = 16843009;
-
-        public static (User, ClaimsPrincipal) SetupUser(
+        public static ClaimsPrincipal SetupUser(
             Mock<IUserManager> userManagerMock,
             Mock<IHttpContextAccessor> httpContextAccessorMock,
             string role,
-            long ip,
             IEnumerable<AccessSchedule>? accessSchedules = null)
         {
             var user = new User(
@@ -72,9 +61,9 @@ namespace Jellyfin.Api.Tests
 
             httpContextAccessorMock
                 .Setup(h => h.HttpContext.Connection.RemoteIpAddress)
-                .Returns(new IPAddress(ip));
+                .Returns(new IPAddress(0));
 
-            return (user, new ClaimsPrincipal(identity));
+            return new ClaimsPrincipal(identity);
         }
 
         public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)

From 4962e230af13933f6a087b78b16884da0e485688 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 17 Jun 2020 06:52:15 -0600
Subject: [PATCH 194/463] revert adding Jellyfin to auth header

---
 .../Security/AuthorizationContext.cs          | 19 +++----------------
 1 file changed, 3 insertions(+), 16 deletions(-)

diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index b9fca67bf0..fb93fae3e6 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -87,11 +87,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 auth.TryGetValue("Token", out token);
             }
 
-            if (string.IsNullOrEmpty(token))
-            {
-                token = headers["X-Jellyfin-Token"];
-            }
-
             if (string.IsNullOrEmpty(token))
             {
                 token = headers["X-Emby-Token"];
@@ -210,11 +205,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
         {
-            var auth = httpReq.Headers["X-Jellyfin-Authorization"];
-            if (string.IsNullOrEmpty(auth))
-            {
-                auth = httpReq.Headers["X-Emby-Authorization"];
-            }
+            var auth = httpReq.Headers["X-Emby-Authorization"];
 
             if (string.IsNullOrEmpty(auth))
             {
@@ -231,11 +222,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
         {
-            var auth = httpReq.Headers["X-Jellyfin-Authorization"];
-            if (string.IsNullOrEmpty(auth))
-            {
-                auth = httpReq.Headers["X-Emby-Authorization"];
-            }
+            var auth = httpReq.Headers["X-Emby-Authorization"];
 
             if (string.IsNullOrEmpty(auth))
             {
@@ -265,7 +252,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 return null;
             }
 
-            var acceptedNames = new[] { "MediaBrowser", "Emby", "Jellyfin" };
+            var acceptedNames = new[] { "MediaBrowser", "Emby" };
 
             // It has to be a digest request
             if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))

From 0c01b6817b9e14661fd1ebea05590b60278e735c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 17 Jun 2020 08:05:30 -0600
Subject: [PATCH 195/463] Add X-Forward-(For/Proto) support

---
 .../Extensions/ApiServiceCollectionExtensions.cs            | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 1ec77d716c..dbd5ba4166 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -18,6 +18,8 @@ using MediaBrowser.Common.Json;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.OpenApi.Models;
 using Swashbuckle.AspNetCore.SwaggerGen;
@@ -105,6 +107,10 @@ namespace Jellyfin.Server.Extensions
                 {
                     options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
                 })
+                .Configure<ForwardedHeadersOptions>(options =>
+                {
+                    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+                })
                 .AddMvc(opts =>
                 {
                     opts.UseGeneralRoutePrefix(baseUrl);

From f181cb374690a9c1af4abffe211ae5e44e4d63b3 Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Wed, 17 Jun 2020 10:41:40 -0600
Subject: [PATCH 196/463] Update Jellyfin.Api/Controllers/FilterController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/FilterController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 431114ea9a..46911ce938 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -159,10 +159,10 @@ namespace Jellyfin.Api.Controllers
                 ? null
                 : _userManager.GetUserById(userId.Value);
 
-            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
             {
                 parentItem = null;
             }

From 0d1298e851d3cdfc56b74f44dc94cfc981a4e8f3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 17 Jun 2020 10:49:34 -0600
Subject: [PATCH 197/463] User proper File constructor

---
 .../Controllers/{Images => }/ImageByNameController.cs     | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
 rename Jellyfin.Api/Controllers/{Images => }/ImageByNameController.cs (96%)

diff --git a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
similarity index 96%
rename from Jellyfin.Api/Controllers/Images/ImageByNameController.cs
rename to Jellyfin.Api/Controllers/ImageByNameController.cs
index db475d6b47..fa46b6dd17 100644
--- a/Jellyfin.Api/Controllers/Images/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
-namespace Jellyfin.Api.Controllers.Images
+namespace Jellyfin.Api.Controllers
 {
     /// <summary>
     ///     Images By Name Controller.
@@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers.Images
             }
 
             var contentType = MimeTypes.GetMimeType(path);
-            return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+            return File(System.IO.File.OpenRead(path), contentType);
         }
 
         /// <summary>
@@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers.Images
                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
                 {
                     var contentType = MimeTypes.GetMimeType(path);
-                    return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+                    return File(System.IO.File.OpenRead(path), contentType);
                 }
             }
 
@@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers.Images
                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
                 {
                     var contentType = MimeTypes.GetMimeType(path);
-                    return new FileStreamResult(System.IO.File.OpenRead(path), contentType);
+                    return File(System.IO.File.OpenRead(path), contentType);
                 }
             }
 

From 9b45ee440cd2d167aee63a05bcbb6137765b4da8 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 17 Jun 2020 10:51:50 -0600
Subject: [PATCH 198/463] User proper File constructor

---
 Jellyfin.Api/Controllers/Images/RemoteImageController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
index f521dfdf28..7c5f17e9e8 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
@@ -191,7 +191,7 @@ namespace Jellyfin.Api.Controllers.Images
             }
 
             var contentType = MimeTypes.GetMimeType(contentPath);
-            return new FileStreamResult(System.IO.File.OpenRead(contentPath), contentType);
+            return File(System.IO.File.OpenRead(contentPath), contentType);
         }
 
         /// <summary>

From f2d7eac4189e99b587f6b7625820fb779d228ecc Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 17 Jun 2020 21:08:58 +0200
Subject: [PATCH 199/463] [WIP] Move UserService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/UserController.cs | 424 +++++++++++++++++++++
 1 file changed, 424 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/UserController.cs

diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
new file mode 100644
index 0000000000..ff9373c2db
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -0,0 +1,424 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Users;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// User controller.
+    /// </summary>
+    [Route("/Users")]
+    public class UserController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ISessionManager _sessionManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IServerConfigurationManager _config;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public UserController(
+            IUserManager userManager,
+            ISessionManager sessionManager,
+            INetworkManager networkManager,
+            IDeviceManager deviceManager,
+            IAuthorizationContext authContext,
+            IServerConfigurationManager config)
+        {
+            _userManager = userManager;
+            _sessionManager = sessionManager;
+            _networkManager = networkManager;
+            _deviceManager = deviceManager;
+            _authContext = authContext;
+            _config = config;
+        }
+
+        /// <summary>
+        /// Gets a list of users.
+        /// </summary>
+        /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
+        /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
+        /// <param name="isGuest">Optional filter by IsGuest=true or false.</param>
+        /// <returns></returns>
+        [HttpGet]
+        [Authorize]
+        public ActionResult<IEnumerable<UserDto>> GetUsers(
+            [FromQuery] bool? isHidden,
+            [FromQuery] bool? isDisabled,
+            [FromQuery] bool? isGuest)
+        {
+            return Ok(Get(isHidden, isDisabled, isGuest, false, false));
+        }
+
+        /// <summary>
+        /// Gets a list of publicly visible users for display on a login screen.
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("Public")]
+        public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
+        {
+            // If the startup wizard hasn't been completed then just return all users
+            if (!_config.Configuration.IsStartupWizardCompleted)
+            {
+                return GetUsers(null, false, null);
+            }
+
+            return Ok(Get(false, false, false, true, true));
+        }
+
+        /// <summary>
+        /// Gets a user by Id.
+        /// </summary>
+        /// <param name="id">The user id.</param>
+        /// <returns></returns>
+        [HttpGet("{id}")]
+        // TODO: authorize escapeParentalControl
+        public ActionResult<UserDto> GetUserById([FromRoute] Guid id)
+        {
+            var user = _userManager.GetUserById(id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
+
+            return Ok(result);
+        }
+
+        /// <summary>
+        /// Deletes a user.
+        /// </summary>
+        /// <param name="id">The user id.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpDelete("{id}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public ActionResult DeleteUser([FromRoute] Guid id)
+        {
+            var user = _userManager.GetUserById(id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            _sessionManager.RevokeUserTokens(user.Id, null);
+            _userManager.DeleteUser(user);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Authenticates a user.
+        /// </summary>
+        /// <param name="id">The user id.</param>
+        /// <param name="pw"></param>
+        /// <param name="password"></param>
+        /// <returns></returns>
+        [HttpPost("{id}/Authenticate")]
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
+            [FromRoute, Required] Guid id,
+            [FromQuery, BindRequired] string pw,
+            [FromQuery, BindRequired] string password)
+        {
+            var user = _userManager.GetUserById(id);
+
+            if (user == null)
+            {
+                return NotFound("User not found");
+            }
+
+            if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
+            {
+                throw new MethodNotAllowedException();
+            }
+
+            // Password should always be null
+            return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Authenticates a user by name.
+        /// </summary>
+        /// <param name="username">The username.</param>
+        /// <param name="pw"></param>
+        /// <param name="password"></param>
+        /// <returns></returns>
+        [HttpPost("AuthenticateByName")]
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName(
+            [FromQuery, BindRequired] string username,
+            [FromQuery, BindRequired] string pw,
+            [FromQuery, BindRequired] string password)
+        {
+            var auth = _authContext.GetAuthorizationInfo(Request);
+
+            try
+            {
+                var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
+                {
+                    App = auth.Client,
+                    AppVersion = auth.Version,
+                    DeviceId = auth.DeviceId,
+                    DeviceName = auth.Device,
+                    Password = pw,
+                    PasswordSha1 = password,
+                    RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(),
+                    Username = username
+                }).ConfigureAwait(false);
+
+                return Ok(result);
+            }
+            catch (SecurityException e)
+            {
+                // rethrow adding IP address to message
+                throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+            }
+        }
+
+        /// <summary>
+        /// Updates a user's password.
+        /// </summary>
+        /// <param name="id"></param>
+        /// <param name="currentPassword"></param>
+        /// <param name="currentPw"></param>
+        /// <param name="newPw"></param>
+        /// <param name="resetPassword">Whether to reset the password.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("{id}/Password")]
+        [Authorize]
+        public async Task<ActionResult> UpdateUserPassword(
+            [FromRoute] Guid id,
+            [FromQuery] string currentPassword,
+            [FromQuery] string currentPw,
+            [FromQuery] string newPw,
+            [FromQuery] bool resetPassword)
+        {
+            AssertCanUpdateUser(_authContext, _userManager, id, true);
+
+            var user = _userManager.GetUserById(id);
+
+            if (user == null)
+            {
+                return NotFound("User not found");
+            }
+
+            if (resetPassword)
+            {
+                await _userManager.ResetPassword(user).ConfigureAwait(false);
+            }
+            else
+            {
+                var success = await _userManager.AuthenticateUser(
+                    user.Username,
+                    currentPw,
+                    currentPassword,
+                    HttpContext.Connection.RemoteIpAddress.ToString(),
+                    false).ConfigureAwait(false);
+
+                if (success == null)
+                {
+                    throw new ArgumentException("Invalid user or password entered.");
+                }
+
+                await _userManager.ChangePassword(user, newPw).ConfigureAwait(false);
+
+                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
+
+                _sessionManager.RevokeUserTokens(user.Id, currentToken);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates a user's easy password.
+        /// </summary>
+        /// <param name="id"></param>
+        /// <param name="newPassword"></param>
+        /// <param name="newPw"></param>
+        /// <param name="resetPassword"></param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("{id}/EasyPassword")]
+        [Authorize]
+        public ActionResult UpdateUserEasyPassword(
+            [FromRoute] Guid id,
+            [FromQuery] string newPassword,
+            [FromQuery] string newPw,
+            [FromQuery] bool resetPassword)
+        {
+            AssertCanUpdateUser(_authContext, _userManager, id, true);
+
+            var user = _userManager.GetUserById(id);
+
+            if (user == null)
+            {
+                return NotFound("User not found");
+            }
+
+            if (resetPassword)
+            {
+                _userManager.ResetEasyPassword(user);
+            }
+            else
+            {
+                _userManager.ChangeEasyPassword(user, newPw, newPassword);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates a user.
+        /// </summary>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("{id}")]
+        [Authorize]
+        public ActionResult UpdateUser() // TODO: missing UserDto
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Updates a user policy.
+        /// </summary>
+        /// <param name="id">The user id.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("{id}/Policy")]
+        [Authorize]
+        public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Updates a user configuration.
+        /// </summary>
+        /// <param name="id">The user id.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("{id}/Configuration")]
+        [Authorize]
+        public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Creates a user.
+        /// </summary>
+        /// <param name="name">The username.</param>
+        /// <param name="password">The password.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("/Users/New")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public async Task<ActionResult> CreateUserByName(
+            [FromBody] string name,
+            [FromBody] string password)
+        {
+            var newUser = _userManager.CreateUser(name);
+
+            // no need to authenticate password for new user
+            if (password != null)
+            {
+                await _userManager.ChangePassword(newUser, password).ConfigureAwait(false);
+            }
+
+            var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
+
+            return Ok(result);
+        }
+
+        /// <summary>
+        /// Initiates the forgot password process for a local user.
+        /// </summary>
+        /// <param name="enteredUsername">The entered username.</param>
+        /// <returns></returns>
+        [HttpPost("ForgotPassword")]
+        public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string enteredUsername)
+        {
+            var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
+                          || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
+
+            var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
+
+            return Ok(result);
+        }
+
+        /// <summary>
+        /// Redeems a forgot password pin.
+        /// </summary>
+        /// <param name="pin">The pin.</param>
+        /// <returns></returns>
+        [HttpPost("ForgotPassword/Pin")]
+        public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin)
+        {
+            var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
+            return Ok(result);
+        }
+
+        private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork)
+        {
+            var users = _userManager.Users;
+
+            if (isDisabled.HasValue)
+            {
+                users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
+            }
+
+            if (isHidden.HasValue)
+            {
+                users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
+            }
+
+            if (filterByDevice)
+            {
+                var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
+
+                if (!string.IsNullOrWhiteSpace(deviceId))
+                {
+                    users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
+                }
+            }
+
+            if (filterByNetwork)
+            {
+                if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()))
+                {
+                    users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
+                }
+            }
+
+            var result = users
+                .OrderBy(u => u.Username)
+                .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()))
+                .ToArray();
+
+            return result;
+        }
+    }
+}

From 9a51f484af3dbbb5717a88fb85473aec78234e32 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 18 Jun 2020 07:11:46 -0600
Subject: [PATCH 200/463] Remove nullable, add async task

---
 .../Controllers/ActivityLogController.cs      |  1 -
 .../Controllers/ConfigurationController.cs    |  2 --
 Jellyfin.Api/Controllers/DevicesController.cs |  2 --
 Jellyfin.Api/Controllers/FilterController.cs  |  3 +--
 .../Controllers/ImageByNameController.cs      |  2 --
 .../Controllers/ItemRefreshController.cs      |  1 -
 .../Controllers/LibraryStructureController.cs | 25 +++++++------------
 .../Controllers/NotificationsController.cs    |  1 -
 Jellyfin.Api/Controllers/PackageController.cs |  2 --
 Jellyfin.Api/Controllers/PluginsController.cs |  3 +--
 .../{Images => }/RemoteImageController.cs     |  4 +--
 .../Controllers/SubtitleController.cs         |  1 -
 .../Controllers/VideoAttachmentsController.cs |  2 --
 .../ConfigurationDtos/MediaEncoderPathDto.cs  |  2 --
 .../NotificationDtos/NotificationDto.cs       |  2 --
 .../NotificationDtos/NotificationResultDto.cs |  2 --
 .../NotificationsSummaryDto.cs                |  2 --
 .../Models/PluginDtos/MBRegistrationRecord.cs |  4 +--
 .../Models/PluginDtos/PluginSecurityInfo.cs   |  4 +--
 .../StartupDtos/StartupConfigurationDto.cs    |  8 +++---
 .../Models/StartupDtos/StartupUserDto.cs      |  6 ++---
 21 files changed, 19 insertions(+), 60 deletions(-)
 rename Jellyfin.Api/Controllers/{Images => }/RemoteImageController.cs (99%)

diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 895d9f719d..4ae7cf5069 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CA1801
 
 using System;
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 780a38aa81..ae5685156e 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 1754b0cbda..1575307c55 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Devices;
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 46911ce938..6a6e6a64a3 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CA1801
+#pragma warning disable CA1801
 
 using System;
 using System.Linq;
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index fa46b6dd17..70f46ffa49 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.IO;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d9b8357d2e..a1df22e411 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CA1801
 
 using System.ComponentModel;
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index a989efe7f1..ca2905b114 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CA1801
 
 using System;
@@ -175,20 +174,18 @@ namespace Jellyfin.Api.Controllers
             {
                 CollectionFolder.OnCollectionFolderChange();
 
-                Task.Run(() =>
+                Task.Run(async () =>
                 {
                     // No need to start if scanning the library because it will handle it
                     if (refreshLibrary)
                     {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                        await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
                     }
                     else
                     {
                         // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
                         // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
+                        await Task.Delay(1000).ConfigureAwait(false);
                         _libraryMonitor.Start();
                     }
                 });
@@ -230,20 +227,18 @@ namespace Jellyfin.Api.Controllers
             }
             finally
             {
-                Task.Run(() =>
+                Task.Run(async () =>
                 {
                     // No need to start if scanning the library because it will handle it
                     if (refreshLibrary)
                     {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                        await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
                     }
                     else
                     {
                         // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
                         // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
+                        await Task.Delay(1000).ConfigureAwait(false);
                         _libraryMonitor.Start();
                     }
                 });
@@ -304,20 +299,18 @@ namespace Jellyfin.Api.Controllers
             }
             finally
             {
-                Task.Run(() =>
+                Task.Run(async () =>
                 {
                     // No need to start if scanning the library because it will handle it
                     if (refreshLibrary)
                     {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+                        await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
                     }
                     else
                     {
                         // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
                         // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
+                        await Task.Delay(1000).ConfigureAwait(false);
                         _libraryMonitor.Start();
                     }
                 });
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index a76675d5a9..a1f9b9e8f7 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CA1801
 
 using System;
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 8200f891c8..4f125f16b4 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 59196a41aa..fdb2f4c35b 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CA1801
+#pragma warning disable CA1801
 
 using System;
 using System.Collections.Generic;
diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
similarity index 99%
rename from Jellyfin.Api/Controllers/Images/RemoteImageController.cs
rename to Jellyfin.Api/Controllers/RemoteImageController.cs
index 7c5f17e9e8..80983ee649 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
-namespace Jellyfin.Api.Controllers.Images
+namespace Jellyfin.Api.Controllers
 {
     /// <summary>
     /// Remote Images Controller.
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 97df8c60d8..caf30031ba 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CA1801
 
 using System;
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 86d9322fe4..268aecad8a 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Net.Mime;
 using System.Threading;
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
index 3706a11e3a..3b827ec12d 100644
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 namespace Jellyfin.Api.Models.ConfigurationDtos
 {
     /// <summary>
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
index 502b22623b..af5239ec2b 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using MediaBrowser.Model.Notifications;
 
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
index e34e176cb9..64e92bd83a 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
index b3746ee2da..0568dea666 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using MediaBrowser.Model.Notifications;
 
 namespace Jellyfin.Api.Models.NotificationDtos
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
index aaaf54267a..7f1255f4b6 100644
--- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
+++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System;
+using System;
 
 namespace Jellyfin.Api.Models.PluginDtos
 {
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
index 793002a6cd..a90398425a 100644
--- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
+++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-namespace Jellyfin.Api.Models.PluginDtos
+namespace Jellyfin.Api.Models.PluginDtos
 {
     /// <summary>
     /// Plugin security info.
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index 5a83a030d2..a5f012245a 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
 namespace Jellyfin.Api.Models.StartupDtos
 {
     /// <summary>
@@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
         /// <summary>
         /// Gets or sets UI language culture.
         /// </summary>
-        public string UICulture { get; set; }
+        public string? UICulture { get; set; }
 
         /// <summary>
         /// Gets or sets the metadata country code.
         /// </summary>
-        public string MetadataCountryCode { get; set; }
+        public string? MetadataCountryCode { get; set; }
 
         /// <summary>
         /// Gets or sets the preferred language for the metadata.
         /// </summary>
-        public string PreferredMetadataLanguage { get; set; }
+        public string? PreferredMetadataLanguage { get; set; }
     }
 }
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
index 0dbb245ec6..e4c9735481 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
 namespace Jellyfin.Api.Models.StartupDtos
 {
     /// <summary>
@@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
         /// <summary>
         /// Gets or sets the username.
         /// </summary>
-        public string Name { get; set; }
+        public string? Name { get; set; }
 
         /// <summary>
         /// Gets or sets the user's password.
         /// </summary>
-        public string Password { get; set; }
+        public string? Password { get; set; }
     }
 }

From 713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 18 Jun 2020 18:09:58 +0200
Subject: [PATCH 201/463] Add xml comments; Add status codes; Use return
 instead of exception

---
 Jellyfin.Api/Controllers/UserController.cs    | 260 ++++++--
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  27 +
 .../Models/UserDtos/AuthenticateUserByName.cs |   9 +
 MediaBrowser.Api/UserService.cs               | 605 ------------------
 4 files changed, 235 insertions(+), 666 deletions(-)
 create mode 100644 Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
 delete mode 100644 MediaBrowser.Api/UserService.cs

diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index ff9373c2db..825219c66a 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -1,11 +1,15 @@
-using System;
+#nullable enable
+#pragma warning disable CA1801
+
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.UserDtos;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
@@ -13,9 +17,11 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Users;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
@@ -65,51 +71,60 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
         /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
         /// <param name="isGuest">Optional filter by IsGuest=true or false.</param>
-        /// <returns></returns>
+        /// <response code="200">Users returned.</response>
+        /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
         [HttpGet]
         [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<UserDto>> GetUsers(
             [FromQuery] bool? isHidden,
             [FromQuery] bool? isDisabled,
             [FromQuery] bool? isGuest)
         {
-            return Ok(Get(isHidden, isDisabled, isGuest, false, false));
+            var users = Get(isHidden, isDisabled, false, false);
+            return Ok(users);
         }
 
         /// <summary>
         /// Gets a list of publicly visible users for display on a login screen.
         /// </summary>
-        /// <returns></returns>
+        /// <response code="200">Public users returned.</response>
+        /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
         [HttpGet("Public")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
         {
             // If the startup wizard hasn't been completed then just return all users
             if (!_config.Configuration.IsStartupWizardCompleted)
             {
-                return GetUsers(null, false, null);
+                return Ok(GetUsers(false, false, false).Value);
             }
 
-            return Ok(Get(false, false, false, true, true));
+            return Ok(Get(false, false, true, true));
         }
 
         /// <summary>
         /// Gets a user by Id.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <returns></returns>
+        /// <response code="200">User returned.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
         [HttpGet("{id}")]
         // TODO: authorize escapeParentalControl
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<UserDto> GetUserById([FromRoute] Guid id)
         {
             var user = _userManager.GetUserById(id);
 
             if (user == null)
             {
-                throw new ResourceNotFoundException("User not found");
+                return NotFound("User not found");
             }
 
             var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
-
             return Ok(result);
         }
 
@@ -117,16 +132,20 @@ namespace Jellyfin.Api.Controllers
         /// Deletes a user.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <response code="200">User deleted.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
         [HttpDelete("{id}")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult DeleteUser([FromRoute] Guid id)
         {
             var user = _userManager.GetUserById(id);
 
             if (user == null)
             {
-                throw new ResourceNotFoundException("User not found");
+                return NotFound("User not found");
             }
 
             _sessionManager.RevokeUserTokens(user.Id, null);
@@ -138,10 +157,16 @@ namespace Jellyfin.Api.Controllers
         /// Authenticates a user.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <param name="pw"></param>
-        /// <param name="password"></param>
-        /// <returns></returns>
+        /// <param name="pw">The password as plain text.</param>
+        /// <param name="password">The password sha1-hash.</param>
+        /// <response code="200">User authenticated.</response>
+        /// <response code="403">Sha1-hashed password only is not allowed.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
         [HttpPost("{id}/Authenticate")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
             [FromRoute, Required] Guid id,
             [FromQuery, BindRequired] string pw,
@@ -156,25 +181,22 @@ namespace Jellyfin.Api.Controllers
 
             if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
             {
-                throw new MethodNotAllowedException();
+                return Forbid("Only sha1 password is not allowed.");
             }
 
             // Password should always be null
-            return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false);
+            return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Authenticates a user by name.
         /// </summary>
-        /// <param name="username">The username.</param>
-        /// <param name="pw"></param>
-        /// <param name="password"></param>
-        /// <returns></returns>
+        /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
+        /// <response code="200">User authenticated.</response>
+        /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
         [HttpPost("AuthenticateByName")]
-        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName(
-            [FromQuery, BindRequired] string username,
-            [FromQuery, BindRequired] string pw,
-            [FromQuery, BindRequired] string password)
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request)
         {
             var auth = _authContext.GetAuthorizationInfo(Request);
 
@@ -186,10 +208,10 @@ namespace Jellyfin.Api.Controllers
                     AppVersion = auth.Version,
                     DeviceId = auth.DeviceId,
                     DeviceName = auth.Device,
-                    Password = pw,
-                    PasswordSha1 = password,
+                    Password = request.Pw,
+                    PasswordSha1 = request.Password,
                     RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(),
-                    Username = username
+                    Username = request.Username
                 }).ConfigureAwait(false);
 
                 return Ok(result);
@@ -204,22 +226,31 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user's password.
         /// </summary>
-        /// <param name="id"></param>
-        /// <param name="currentPassword"></param>
-        /// <param name="currentPw"></param>
-        /// <param name="newPw"></param>
+        /// <param name="id">The user id.</param>
+        /// <param name="currentPassword">The current password sha1-hash.</param>
+        /// <param name="currentPw">The current password as plain text.</param>
+        /// <param name="newPw">The new password in plain text.</param>
         /// <param name="resetPassword">Whether to reset the password.</param>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <response code="200">Password successfully reset.</response>
+        /// <response code="403">User is not allowed to update the password.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
         [HttpPost("{id}/Password")]
         [Authorize]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> UpdateUserPassword(
             [FromRoute] Guid id,
-            [FromQuery] string currentPassword,
-            [FromQuery] string currentPw,
-            [FromQuery] string newPw,
-            [FromQuery] bool resetPassword)
+            [FromBody] string currentPassword,
+            [FromBody] string currentPw,
+            [FromBody] string newPw,
+            [FromBody] bool resetPassword)
         {
-            AssertCanUpdateUser(_authContext, _userManager, id, true);
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
+            {
+                return Forbid("User is not allowed to update the password.");
+            }
 
             var user = _userManager.GetUserById(id);
 
@@ -243,7 +274,7 @@ namespace Jellyfin.Api.Controllers
 
                 if (success == null)
                 {
-                    throw new ArgumentException("Invalid user or password entered.");
+                    return Forbid("Invalid user or password entered.");
                 }
 
                 await _userManager.ChangePassword(user, newPw).ConfigureAwait(false);
@@ -259,20 +290,29 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user's easy password.
         /// </summary>
-        /// <param name="id"></param>
-        /// <param name="newPassword"></param>
-        /// <param name="newPw"></param>
-        /// <param name="resetPassword"></param>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <param name="id">The user id.</param>
+        /// <param name="newPassword">The new password sha1-hash.</param>
+        /// <param name="newPw">The new password in plain text.</param>
+        /// <param name="resetPassword">Whether to reset the password.</param>
+        /// <response code="200">Password successfully reset.</response>
+        /// <response code="403">User is not allowed to update the password.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
         [HttpPost("{id}/EasyPassword")]
         [Authorize]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateUserEasyPassword(
             [FromRoute] Guid id,
-            [FromQuery] string newPassword,
-            [FromQuery] string newPw,
-            [FromQuery] bool resetPassword)
+            [FromBody] string newPassword,
+            [FromBody] string newPw,
+            [FromBody] bool resetPassword)
         {
-            AssertCanUpdateUser(_authContext, _userManager, id, true);
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
+            {
+                return Forbid("User is not allowed to update the easy password.");
+            }
 
             var user = _userManager.GetUserById(id);
 
@@ -296,36 +336,128 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user.
         /// </summary>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <param name="id">The user id.</param>
+        /// <param name="updateUser">The updated user model.</param>
+        /// <response code="204">User updated.</response>
+        /// <response code="400">User information was not supplied.</response>
+        /// <response code="403">User update forbidden.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
         [HttpPost("{id}")]
         [Authorize]
-        public ActionResult UpdateUser() // TODO: missing UserDto
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status400BadRequest)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public async Task<ActionResult> UpdateUser(
+            [FromRoute] Guid id,
+            [FromBody] UserDto updateUser)
         {
-            throw new NotImplementedException();
+            if (updateUser == null)
+            {
+                return BadRequest();
+            }
+
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false))
+            {
+                return Forbid("User update not allowed.");
+            }
+
+            var user = _userManager.GetUserById(id);
+
+            if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
+            {
+                await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+                _userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
+            }
+            else
+            {
+                await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
+                _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
+            }
+
+            return NoContent();
         }
 
         /// <summary>
         /// Updates a user policy.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <param name="newPolicy">The new user policy.</param>
+        /// <response code="204">User policy updated.</response>
+        /// <response code="400">User policy was not supplied.</response>
+        /// <response code="403">User policy update forbidden.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
         [HttpPost("{id}/Policy")]
         [Authorize]
-        public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status400BadRequest)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public ActionResult UpdateUserPolicy(
+            [FromRoute] Guid id,
+            [FromBody] UserPolicy newPolicy)
         {
-            throw new NotImplementedException();
+            if (newPolicy == null)
+            {
+                return BadRequest();
+            }
+
+            var user = _userManager.GetUserById(id);
+
+            // If removing admin access
+            if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)))
+            {
+                if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
+                {
+                    return Forbid("There must be at least one user in the system with administrative access.");
+                }
+            }
+
+            // If disabling
+            if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
+            {
+                return Forbid("Administrators cannot be disabled.");
+            }
+
+            // If disabling
+            if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
+            {
+                if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
+                {
+                    return Forbid("There must be at least one enabled user in the system.");
+                }
+
+                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
+                _sessionManager.RevokeUserTokens(user.Id, currentToken);
+            }
+
+            _userManager.UpdatePolicy(id, newPolicy);
+
+            return NoContent();
         }
 
         /// <summary>
         /// Updates a user configuration.
         /// </summary>
         /// <param name="id">The user id.</param>
+        /// <param name="userConfig">The new user configuration.</param>
+        /// <response code="204">User configuration updated.</response>
+        /// <response code="403">User configuration update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{id}/Configuration")]
         [Authorize]
-        public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public ActionResult UpdateUserConfiguration(
+            [FromRoute] Guid id,
+            [FromBody] UserConfiguration userConfig)
         {
-            throw new NotImplementedException();
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false))
+            {
+                return Forbid("User configuration update not allowed");
+            }
+
+            _userManager.UpdateConfiguration(id, userConfig);
+
+            return NoContent();
         }
 
         /// <summary>
@@ -333,10 +465,12 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="name">The username.</param>
         /// <param name="password">The password.</param>
-        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        /// <response code="200">User created.</response>
+        /// <returns>An <see cref="UserDto"/> of the new user.</returns>
         [HttpPost("/Users/New")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        public async Task<ActionResult> CreateUserByName(
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<UserDto>> CreateUserByName(
             [FromBody] string name,
             [FromBody] string password)
         {
@@ -357,8 +491,10 @@ namespace Jellyfin.Api.Controllers
         /// Initiates the forgot password process for a local user.
         /// </summary>
         /// <param name="enteredUsername">The entered username.</param>
-        /// <returns></returns>
+        /// <response code="200">Password reset process started.</response>
+        /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
         [HttpPost("ForgotPassword")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string enteredUsername)
         {
             var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
@@ -373,15 +509,17 @@ namespace Jellyfin.Api.Controllers
         /// Redeems a forgot password pin.
         /// </summary>
         /// <param name="pin">The pin.</param>
-        /// <returns></returns>
+        /// <response code="200">Pin reset process started.</response>
+        /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
         [HttpPost("ForgotPassword/Pin")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin)
         {
             var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
             return Ok(result);
         }
 
-        private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork)
+        private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
         {
             var users = _userManager.Users;
 
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 9f4d34f9c6..6d6acbcf91 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,4 +1,7 @@
 using System;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
 {
@@ -25,5 +28,29 @@ namespace Jellyfin.Api.Helpers
                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
                 : value.Split(separator);
         }
+
+        /// <summary>
+        /// Checks if the user can update an entry.
+        /// </summary>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="requestContext">The <see cref="HttpRequest"/>.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
+        /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
+        internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
+        {
+            var auth = authContext.GetAuthorizationInfo(requestContext);
+
+            var authenticatedUser = auth.User;
+
+            // If they're going to update the record of another user, they must be an administrator
+            if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
+                || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
+            {
+                return false;
+            }
+
+            return true;
+        }
     }
 }
diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
new file mode 100644
index 0000000000..00b90a9250
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -0,0 +1,9 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+    public class AuthenticateUserByName
+    {
+        public string Username { get; set; }
+        public string Pw { get; set; }
+        public string Password { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
deleted file mode 100644
index 9cb9baf631..0000000000
--- a/MediaBrowser.Api/UserService.cs
+++ /dev/null
@@ -1,605 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetUsers
-    /// </summary>
-    [Route("/Users", "GET", Summary = "Gets a list of users")]
-    [Authenticated]
-    public class GetUsers : IReturn<UserDto[]>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-
-        [ApiMember(Name = "IsDisabled", Description = "Optional filter by IsDisabled=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsDisabled { get; set; }
-
-        [ApiMember(Name = "IsGuest", Description = "Optional filter by IsGuest=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsGuest { get; set; }
-    }
-
-    [Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")]
-    public class GetPublicUsers : IReturn<UserDto[]>
-    {
-    }
-
-    /// <summary>
-    /// Class GetUser
-    /// </summary>
-    [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
-    [Authenticated(EscapeParentalControl = true)]
-    public class GetUser : IReturn<UserDto>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class DeleteUser
-    /// </summary>
-    [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")]
-    [Authenticated(Roles = "Admin")]
-    public class DeleteUser : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class AuthenticateUser
-    /// </summary>
-    [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")]
-    public class AuthenticateUser : IReturn<AuthenticationResult>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pw { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-    }
-
-    /// <summary>
-    /// Class AuthenticateUser
-    /// </summary>
-    [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")]
-    public class AuthenticateUserByName : IReturn<AuthenticationResult>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Username { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-
-        [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pw { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserPassword
-    /// </summary>
-    [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")]
-    [Authenticated]
-    public class UpdateUserPassword : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        public string CurrentPassword { get; set; }
-
-        public string CurrentPw { get; set; }
-
-        public string NewPw { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [reset password].
-        /// </summary>
-        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
-        public bool ResetPassword { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserEasyPassword
-    /// </summary>
-    [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")]
-    [Authenticated]
-    public class UpdateUserEasyPassword : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the new password.
-        /// </summary>
-        /// <value>The new password.</value>
-        public string NewPassword { get; set; }
-
-        public string NewPw { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [reset password].
-        /// </summary>
-        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
-        public bool ResetPassword { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUser
-    /// </summary>
-    [Route("/Users/{Id}", "POST", Summary = "Updates a user")]
-    [Authenticated]
-    public class UpdateUser : UserDto, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class UpdateUser
-    /// </summary>
-    [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")]
-    [Authenticated(Roles = "admin")]
-    public class UpdateUserPolicy : UserPolicy, IReturnVoid
-    {
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUser
-    /// </summary>
-    [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")]
-    [Authenticated]
-    public class UpdateUserConfiguration : UserConfiguration, IReturnVoid
-    {
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class CreateUser
-    /// </summary>
-    [Route("/Users/New", "POST", Summary = "Creates a user")]
-    [Authenticated(Roles = "Admin")]
-    public class CreateUserByName : IReturn<UserDto>
-    {
-        [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-    }
-
-    [Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")]
-    public class ForgotPassword : IReturn<ForgotPasswordResult>
-    {
-        [ApiMember(Name = "EnteredUsername", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string EnteredUsername { get; set; }
-    }
-
-    [Route("/Users/ForgotPassword/Pin", "POST", Summary = "Redeems a forgot password pin")]
-    public class ForgotPasswordPin : IReturn<PinRedeemResult>
-    {
-        [ApiMember(Name = "Pin", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pin { get; set; }
-    }
-
-    /// <summary>
-    /// Class UsersService
-    /// </summary>
-    public class UserService : BaseApiService
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-        private readonly ISessionManager _sessionMananger;
-        private readonly INetworkManager _networkManager;
-        private readonly IDeviceManager _deviceManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public UserService(
-            ILogger<UserService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ISessionManager sessionMananger,
-            INetworkManager networkManager,
-            IDeviceManager deviceManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _sessionMananger = sessionMananger;
-            _networkManager = networkManager;
-            _deviceManager = deviceManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetPublicUsers request)
-        {
-            // If the startup wizard hasn't been completed then just return all users
-            if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted)
-            {
-                return Get(new GetUsers
-                {
-                    IsDisabled = false
-                });
-            }
-
-            return Get(new GetUsers
-            {
-                IsHidden = false,
-                IsDisabled = false
-            }, true, true);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetUsers request)
-        {
-            return Get(request, false, false);
-        }
-
-        private object Get(GetUsers request, bool filterByDevice, bool filterByNetwork)
-        {
-            var users = _userManager.Users;
-
-            if (request.IsDisabled.HasValue)
-            {
-                users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value);
-            }
-
-            if (request.IsHidden.HasValue)
-            {
-                users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value);
-            }
-
-            if (filterByDevice)
-            {
-                var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
-
-                if (!string.IsNullOrWhiteSpace(deviceId))
-                {
-                    users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
-                }
-            }
-
-            if (filterByNetwork)
-            {
-                if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
-                {
-                    users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
-                }
-            }
-
-            var result = users
-                .OrderBy(u => u.Username)
-                .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
-                .ToArray();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetUser request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            var result = _userManager.GetUserDto(user, Request.RemoteIp);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(DeleteUser request)
-        {
-            return DeleteAsync(request);
-        }
-
-        public Task DeleteAsync(DeleteUser request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            _sessionMananger.RevokeUserTokens(user.Id, null);
-            _userManager.DeleteUser(user);
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(AuthenticateUser request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
-            {
-                throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
-            }
-
-            // Password should always be null
-            return Post(new AuthenticateUserByName
-            {
-                Username = user.Username,
-                Password = null,
-                Pw = request.Pw
-            });
-        }
-
-        public async Task<object> Post(AuthenticateUserByName request)
-        {
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            try
-            {
-                var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
-                {
-                    App = auth.Client,
-                    AppVersion = auth.Version,
-                    DeviceId = auth.DeviceId,
-                    DeviceName = auth.Device,
-                    Password = request.Pw,
-                    PasswordSha1 = request.Password,
-                    RemoteEndPoint = Request.RemoteIp,
-                    Username = request.Username
-                }).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-            catch (SecurityException e)
-            {
-                // rethrow adding IP address to message
-                throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(UpdateUserPassword request)
-        {
-            return PostAsync(request);
-        }
-
-        public async Task PostAsync(UpdateUserPassword request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (request.ResetPassword)
-            {
-                await _userManager.ResetPassword(user).ConfigureAwait(false);
-            }
-            else
-            {
-                var success = await _userManager.AuthenticateUser(
-                    user.Username,
-                    request.CurrentPw,
-                    request.CurrentPassword,
-                    Request.RemoteIp,
-                    false).ConfigureAwait(false);
-
-                if (success == null)
-                {
-                    throw new ArgumentException("Invalid user or password entered.");
-                }
-
-                await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
-
-                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
-
-                _sessionMananger.RevokeUserTokens(user.Id, currentToken);
-            }
-        }
-
-        public void Post(UpdateUserEasyPassword request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (request.ResetPassword)
-            {
-                _userManager.ResetEasyPassword(user);
-            }
-            else
-            {
-                _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(UpdateUser request)
-        {
-            var id = Guid.Parse(GetPathValue(1));
-
-            AssertCanUpdateUser(_authContext, _userManager, id, false);
-
-            var dtoUser = request;
-
-            var user = _userManager.GetUserById(id);
-
-            if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal))
-            {
-                await _userManager.UpdateUserAsync(user);
-                _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration);
-            }
-            else
-            {
-                await _userManager.RenameUser(user, dtoUser.Name).ConfigureAwait(false);
-
-                _userManager.UpdateConfiguration(dtoUser.Id, dtoUser.Configuration);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Post(CreateUserByName request)
-        {
-            var newUser = _userManager.CreateUser(request.Name);
-
-            // no need to authenticate password for new user
-            if (request.Password != null)
-            {
-                await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
-            }
-
-            var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(ForgotPassword request)
-        {
-            var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp);
-
-            var result = await _userManager.StartForgotPasswordProcess(request.EnteredUsername, isLocal).ConfigureAwait(false);
-
-            return result;
-        }
-
-        public async Task<object> Post(ForgotPasswordPin request)
-        {
-            var result = await _userManager.RedeemPasswordResetPin(request.Pin).ConfigureAwait(false);
-
-            return result;
-        }
-
-        public void Post(UpdateUserConfiguration request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, false);
-
-            _userManager.UpdateConfiguration(request.Id, request);
-        }
-
-        public void Post(UpdateUserPolicy request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            // If removing admin access
-            if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
-            {
-                if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
-                {
-                    throw new ArgumentException("There must be at least one user in the system with administrative access.");
-                }
-            }
-
-            // If disabling
-            if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
-            {
-                throw new ArgumentException("Administrators cannot be disabled.");
-            }
-
-            // If disabling
-            if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
-            {
-                if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
-                {
-                    throw new ArgumentException("There must be at least one enabled user in the system.");
-                }
-
-                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
-                _sessionMananger.RevokeUserTokens(user.Id, currentToken);
-            }
-
-            _userManager.UpdatePolicy(request.Id, request);
-        }
-    }
-}

From 69e1047bf30d672cf948e9feaae891e9934f6920 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 18 Jun 2020 10:42:48 -0600
Subject: [PATCH 202/463] Add DtoExtensions.cs

---
 Jellyfin.Api/Extensions/DtoExtensions.cs | 162 +++++++++++++++++++++++
 1 file changed, 162 insertions(+)
 create mode 100644 Jellyfin.Api/Extensions/DtoExtensions.cs

diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
new file mode 100644
index 0000000000..4c587391fc
--- /dev/null
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Extensions
+{
+    /// <summary>
+    /// Dto Extensions.
+    /// </summary>
+    public static class DtoExtensions
+    {
+        /// <summary>
+        /// Add Dto Item fields.
+        /// </summary>
+        /// <remarks>
+        /// Converted from IHasItemFields.
+        /// Legacy order: 1.
+        /// </remarks>
+        /// <param name="dtoOptions">DtoOptions object.</param>
+        /// <param name="fields">Comma delimited string of fields.</param>
+        /// <returns>Modified DtoOptions object.</returns>
+        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields)
+        {
+            if (string.IsNullOrEmpty(fields))
+            {
+                dtoOptions.Fields = Array.Empty<ItemFields>();
+            }
+            else
+            {
+                dtoOptions.Fields = fields.Split(',')
+                    .Select(v =>
+                    {
+                        if (Enum.TryParse(v, true, out ItemFields value))
+                        {
+                            return (ItemFields?)value;
+                        }
+
+                        return null;
+                    })
+                    .Where(i => i.HasValue)
+                    .Select(i => i!.Value)
+                    .ToArray();
+            }
+
+            return dtoOptions;
+        }
+
+        /// <summary>
+        /// Add additional fields depending on client.
+        /// </summary>
+        /// <remarks>
+        /// Use in place of GetDtoOptions.
+        /// Legacy order: 2.
+        /// </remarks>
+        /// <param name="dtoOptions">DtoOptions object.</param>
+        /// <param name="request">Current request.</param>
+        /// <returns>Modified DtoOptions object.</returns>
+        internal static DtoOptions AddClientFields(
+            this DtoOptions dtoOptions, HttpRequest request)
+        {
+            dtoOptions.Fields ??= Array.Empty<ItemFields>();
+
+            string? client = ClaimHelpers.GetClient(request.HttpContext.User);
+
+            // No client in claim
+            if (string.IsNullOrEmpty(client))
+            {
+                return dtoOptions;
+            }
+
+            if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
+            {
+                if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    int oldLen = dtoOptions.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    dtoOptions.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = ItemFields.RecursiveItemCount;
+                    dtoOptions.Fields = arr;
+                }
+            }
+
+            if (!dtoOptions.ContainsField(ItemFields.ChildCount))
+            {
+                if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
+                    client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    int oldLen = dtoOptions.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    dtoOptions.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = ItemFields.ChildCount;
+                    dtoOptions.Fields = arr;
+                }
+            }
+
+            return dtoOptions;
+        }
+
+        /// <summary>
+        /// Add additional DtoOptions.
+        /// </summary>
+        /// <remarks>
+        /// Converted from IHasDtoOptions.
+        /// Legacy order: 3.
+        /// </remarks>
+        /// <param name="dtoOptions">DtoOptions object.</param>
+        /// <param name="enableImages">Enable images.</param>
+        /// <param name="enableUserData">Enable user data.</param>
+        /// <param name="imageTypeLimit">Image type limit.</param>
+        /// <param name="enableImageTypes">Enable image types.</param>
+        /// <returns>Modified DtoOptions object.</returns>
+        internal static DtoOptions AddAdditionalDtoOptions(
+            in DtoOptions dtoOptions,
+            bool? enableImages,
+            bool? enableUserData,
+            int? imageTypeLimit,
+            string enableImageTypes)
+        {
+            dtoOptions.EnableImages = enableImages ?? true;
+
+            if (imageTypeLimit.HasValue)
+            {
+                dtoOptions.ImageTypeLimit = imageTypeLimit.Value;
+            }
+
+            if (enableUserData.HasValue)
+            {
+                dtoOptions.EnableUserData = enableUserData.Value;
+            }
+
+            if (!string.IsNullOrWhiteSpace(enableImageTypes))
+            {
+                dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+                    .ToArray();
+            }
+
+            return dtoOptions;
+        }
+
+        /// <summary>
+        /// Check if DtoOptions contains field.
+        /// </summary>
+        /// <param name="dtoOptions">DtoOptions object.</param>
+        /// <param name="field">Field to check.</param>
+        /// <returns>Field existence.</returns>
+        internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
+            => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
+    }
+}

From 77bea567082528be3d1da09ed214ec0a1e192a97 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 18 Jun 2020 19:35:29 +0200
Subject: [PATCH 203/463] Add request body models

---
 Jellyfin.Api/Controllers/UserController.cs    | 54 ++++++++-----------
 .../Models/UserDtos/AuthenticateUserByName.cs | 20 +++++--
 .../Models/UserDtos/CreateUserByName.cs       | 18 +++++++
 .../Models/UserDtos/UpdateUserEasyPassword.cs | 23 ++++++++
 .../Models/UserDtos/UpdateUserPassword.cs     | 28 ++++++++++
 5 files changed, 109 insertions(+), 34 deletions(-)
 create mode 100644 Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
 create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
 create mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs

diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 825219c66a..24123085bf 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -111,8 +111,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">User not found.</response>
         /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
         [HttpGet("{id}")]
-        // TODO: authorize escapeParentalControl
-        [Authorize]
+        [Authorize(Policy = Policies.IgnoreSchedule)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<UserDto> GetUserById([FromRoute] Guid id)
@@ -185,7 +184,13 @@ namespace Jellyfin.Api.Controllers
             }
 
             // Password should always be null
-            return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false);
+            AuthenticateUserByName request = new AuthenticateUserByName
+            {
+                Username = user.Username,
+                Password = null,
+                Pw = pw
+            };
+            return await AuthenticateUserByName(request).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -227,10 +232,7 @@ namespace Jellyfin.Api.Controllers
         /// Updates a user's password.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <param name="currentPassword">The current password sha1-hash.</param>
-        /// <param name="currentPw">The current password as plain text.</param>
-        /// <param name="newPw">The new password in plain text.</param>
-        /// <param name="resetPassword">Whether to reset the password.</param>
+        /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
         /// <response code="200">Password successfully reset.</response>
         /// <response code="403">User is not allowed to update the password.</response>
         /// <response code="404">User not found.</response>
@@ -242,10 +244,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> UpdateUserPassword(
             [FromRoute] Guid id,
-            [FromBody] string currentPassword,
-            [FromBody] string currentPw,
-            [FromBody] string newPw,
-            [FromBody] bool resetPassword)
+            [FromBody] UpdateUserPassword request)
         {
             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
             {
@@ -259,7 +258,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound("User not found");
             }
 
-            if (resetPassword)
+            if (request.ResetPassword)
             {
                 await _userManager.ResetPassword(user).ConfigureAwait(false);
             }
@@ -267,8 +266,8 @@ namespace Jellyfin.Api.Controllers
             {
                 var success = await _userManager.AuthenticateUser(
                     user.Username,
-                    currentPw,
-                    currentPassword,
+                    request.CurrentPw,
+                    request.CurrentPw,
                     HttpContext.Connection.RemoteIpAddress.ToString(),
                     false).ConfigureAwait(false);
 
@@ -277,7 +276,7 @@ namespace Jellyfin.Api.Controllers
                     return Forbid("Invalid user or password entered.");
                 }
 
-                await _userManager.ChangePassword(user, newPw).ConfigureAwait(false);
+                await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
 
                 var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
 
@@ -291,9 +290,7 @@ namespace Jellyfin.Api.Controllers
         /// Updates a user's easy password.
         /// </summary>
         /// <param name="id">The user id.</param>
-        /// <param name="newPassword">The new password sha1-hash.</param>
-        /// <param name="newPw">The new password in plain text.</param>
-        /// <param name="resetPassword">Whether to reset the password.</param>
+        /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
         /// <response code="200">Password successfully reset.</response>
         /// <response code="403">User is not allowed to update the password.</response>
         /// <response code="404">User not found.</response>
@@ -305,9 +302,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateUserEasyPassword(
             [FromRoute] Guid id,
-            [FromBody] string newPassword,
-            [FromBody] string newPw,
-            [FromBody] bool resetPassword)
+            [FromBody] UpdateUserEasyPassword request)
         {
             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
             {
@@ -321,13 +316,13 @@ namespace Jellyfin.Api.Controllers
                 return NotFound("User not found");
             }
 
-            if (resetPassword)
+            if (request.ResetPassword)
             {
                 _userManager.ResetEasyPassword(user);
             }
             else
             {
-                _userManager.ChangeEasyPassword(user, newPw, newPassword);
+                _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
             }
 
             return NoContent();
@@ -463,23 +458,20 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Creates a user.
         /// </summary>
-        /// <param name="name">The username.</param>
-        /// <param name="password">The password.</param>
+        /// <param name="request">The create user by name request body.</param>
         /// <response code="200">User created.</response>
         /// <returns>An <see cref="UserDto"/> of the new user.</returns>
         [HttpPost("/Users/New")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<UserDto>> CreateUserByName(
-            [FromBody] string name,
-            [FromBody] string password)
+        public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
         {
-            var newUser = _userManager.CreateUser(name);
+            var newUser = _userManager.CreateUser(request.Name);
 
             // no need to authenticate password for new user
-            if (password != null)
+            if (request.Password != null)
             {
-                await _userManager.ChangePassword(newUser, password).ConfigureAwait(false);
+                await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
             }
 
             var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
index 00b90a9250..3936274356 100644
--- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -1,9 +1,23 @@
 namespace Jellyfin.Api.Models.UserDtos
 {
+    /// <summary>
+    /// The authenticate user by name request body.
+    /// </summary>
     public class AuthenticateUserByName
     {
-        public string Username { get; set; }
-        public string Pw { get; set; }
-        public string Password { get; set; }
+        /// <summary>
+        /// Gets or sets the username.
+        /// </summary>
+        public string? Username { get; set; }
+
+        /// <summary>
+        /// Gets or sets the plain text password.
+        /// </summary>
+        public string? Pw { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sha1-hashed password.
+        /// </summary>
+        public string? Password { get; set; }
     }
 }
diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
new file mode 100644
index 0000000000..1c88d36287
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+    /// <summary>
+    /// The create user by name request body.
+    /// </summary>
+    public class CreateUserByName
+    {
+        /// <summary>
+        /// Gets or sets the username.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the password.
+        /// </summary>
+        public string? Password { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
new file mode 100644
index 0000000000..0a173ea1a9
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+    /// <summary>
+    /// The update user easy password request body.
+    /// </summary>
+    public class UpdateUserEasyPassword
+    {
+        /// <summary>
+        /// Gets or sets the new sha1-hashed password.
+        /// </summary>
+        public string? NewPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets the new password.
+        /// </summary>
+        public string? NewPw { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to reset the password.
+        /// </summary>
+        public bool ResetPassword { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
new file mode 100644
index 0000000000..8288dbbc44
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/UpdateUserPassword.cs
@@ -0,0 +1,28 @@
+namespace Jellyfin.Api.Models.UserDtos
+{
+    /// <summary>
+    /// The update user password request body.
+    /// </summary>
+    public class UpdateUserPassword
+    {
+        /// <summary>
+        /// Gets or sets the current sha1-hashed password.
+        /// </summary>
+        public string? CurrentPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets the current plain text password.
+        /// </summary>
+        public string? CurrentPw { get; set; }
+
+        /// <summary>
+        /// Gets or sets the new plain text password.
+        /// </summary>
+        public string? NewPw { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to reset the password.
+        /// </summary>
+        public bool ResetPassword { get; set; }
+    }
+}

From 6651cb8d24f0de690b3be68db7c0b78e2413534f Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 12:24:39 +0200
Subject: [PATCH 204/463] Add JsonInto32Converter Add additional swagger type
 mapping

---
 .../ApiServiceCollectionExtensions.cs         | 13 +++++++
 .../Json/Converters/JsonInt32Converter.cs     | 37 ++++++-------------
 MediaBrowser.Common/Json/JsonDefaults.cs      |  1 +
 3 files changed, 26 insertions(+), 25 deletions(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index dbd5ba4166..821a52e476 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -215,6 +215,19 @@ namespace Jellyfin.Server.Extensions
                             Format = "string"
                         })
                 });
+
+            options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
+                new OpenApiSchema
+                {
+                    Type = "object",
+                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
+                        name => name,
+                        name => new OpenApiSchema
+                        {
+                            Type = "string",
+                            Format = "string"
+                        })
+                });
         }
     }
 }
diff --git a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
index fe5dd6cd4d..70c375b8cd 100644
--- a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
@@ -14,40 +14,27 @@ namespace MediaBrowser.Common.Json.Converters
         /// <inheritdoc />
         public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
-            static void ThrowFormatException() => throw new FormatException("Invalid format for an integer.");
-            ReadOnlySpan<byte> span = stackalloc byte[0];
-
-            if (reader.HasValueSequence)
-            {
-                long sequenceLength = reader.ValueSequence.Length;
-                Span<byte> stackSpan = stackalloc byte[(int)sequenceLength];
-                reader.ValueSequence.CopyTo(stackSpan);
-                span = stackSpan;
-            }
-            else
+            if (reader.TokenType == JsonTokenType.String)
             {
-                span = reader.ValueSpan;
-            }
+                ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+                if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed)
+                {
+                    return number;
+                }
 
-            if (!Utf8Parser.TryParse(span, out int number, out _))
-            {
-                ThrowFormatException();
+                if (int.TryParse(reader.GetString(), out number))
+                {
+                    return number;
+                }
             }
 
-            return number;
+            return reader.GetInt32();
         }
 
         /// <inheritdoc />
         public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
         {
-            static void ThrowInvalidOperationException() => throw new InvalidOperationException();
-            Span<byte> span = stackalloc byte[16];
-            if (Utf8Formatter.TryFormat(value, span, out int bytesWritten))
-            {
-                writer.WriteStringValue(span.Slice(0, bytesWritten));
-            }
-
-            ThrowInvalidOperationException();
+            writer.WriteNumberValue(value);
         }
     }
 }
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index adc15123b1..ec3c45476c 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Common.Json
             };
 
             options.Converters.Add(new JsonGuidConverter());
+            options.Converters.Add(new JsonInt32Converter());
             options.Converters.Add(new JsonStringEnumConverter());
             options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
 

From a5bd7f2d6ee0fef67c34f61db9be36167c30d890 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 13:03:53 +0200
Subject: [PATCH 205/463] Use new authorization and session functions

---
 Jellyfin.Api/Controllers/SessionController.cs | 32 ++++++++-----------
 Jellyfin.Api/Helpers/RequestHelpers.cs        | 14 ++++++--
 2 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 5b60275eb6..4f259536a1 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
@@ -27,7 +28,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IUserManager _userManager;
         private readonly IAuthorizationContext _authContext;
         private readonly IDeviceManager _deviceManager;
-        private readonly ISessionContext _sessionContext;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionController"/> class.
@@ -36,19 +36,16 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
         /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
-        /// <param name="sessionContext">Instance of <see cref="ISessionContext"/> interface.</param>
         public SessionController(
             ISessionManager sessionManager,
             IUserManager userManager,
             IAuthorizationContext authContext,
-            IDeviceManager deviceManager,
-            ISessionContext sessionContext)
+            IDeviceManager deviceManager)
         {
             _sessionManager = sessionManager;
             _userManager = userManager;
             _authContext = authContext;
             _deviceManager = deviceManager;
-            _sessionContext = sessionContext;
         }
 
         /// <summary>
@@ -80,12 +77,12 @@ namespace Jellyfin.Api.Controllers
 
                 var user = _userManager.GetUserById(controllableByUserId);
 
-                if (!user.Policy.EnableRemoteControlOfOtherUsers)
+                if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
                 {
                     result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId));
                 }
 
-                if (!user.Policy.EnableSharedDeviceControl)
+                if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
                 {
                     result = result.Where(i => !i.UserId.Equals(Guid.Empty));
                 }
@@ -138,7 +135,7 @@ namespace Jellyfin.Api.Controllers
             };
 
             _sessionManager.SendBrowseCommand(
-                RequestHelpers.GetSession(_sessionContext).Id,
+                RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
                 id,
                 command,
                 CancellationToken.None);
@@ -175,7 +172,7 @@ namespace Jellyfin.Api.Controllers
             playRequest.PlayCommand = playCommand;
 
             _sessionManager.SendPlayCommand(
-                RequestHelpers.GetSession(_sessionContext).Id,
+                RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
                 id,
                 playRequest,
                 CancellationToken.None);
@@ -197,7 +194,7 @@ namespace Jellyfin.Api.Controllers
             [FromBody] PlaystateRequest playstateRequest)
         {
             _sessionManager.SendPlaystateCommand(
-                RequestHelpers.GetSession(_sessionContext).Id,
+                RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
                 id,
                 playstateRequest,
                 CancellationToken.None);
@@ -224,7 +221,7 @@ namespace Jellyfin.Api.Controllers
                 name = commandType.ToString();
             }
 
-            var currentSession = RequestHelpers.GetSession(_sessionContext);
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
             var generalCommand = new GeneralCommand
             {
                 Name = name,
@@ -249,7 +246,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] string id,
             [FromRoute] string command)
         {
-            var currentSession = RequestHelpers.GetSession(_sessionContext);
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
             var generalCommand = new GeneralCommand
             {
@@ -275,7 +272,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] string id,
             [FromBody, Required] GeneralCommand command)
         {
-            var currentSession = RequestHelpers.GetSession(_sessionContext);
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
             if (command == null)
             {
@@ -317,7 +314,7 @@ namespace Jellyfin.Api.Controllers
                 Text = text
             };
 
-            _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionContext).Id, id, command, CancellationToken.None);
+            _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None);
 
             return NoContent();
         }
@@ -379,7 +376,7 @@ namespace Jellyfin.Api.Controllers
         {
             if (string.IsNullOrWhiteSpace(id))
             {
-                id = RequestHelpers.GetSession(_sessionContext).Id;
+                id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
             }
 
             _sessionManager.ReportCapabilities(id, new ClientCapabilities
@@ -408,7 +405,7 @@ namespace Jellyfin.Api.Controllers
         {
             if (string.IsNullOrWhiteSpace(id))
             {
-                id = RequestHelpers.GetSession(_sessionContext).Id;
+                id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
             }
 
             _sessionManager.ReportCapabilities(id, capabilities);
@@ -429,7 +426,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string sessionId,
             [FromQuery] string itemId)
         {
-            string session = RequestHelpers.GetSession(_sessionContext).Id;
+            string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
 
             _sessionManager.ReportNowViewingItem(session, itemId);
             return NoContent();
@@ -444,7 +441,6 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportSessionEnded()
         {
-            // TODO: how do we get AuthorizationInfo without an IRequest?
             AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
 
             _sessionManager.Logout(auth.Token);
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index ae8ab37e8e..2aa700de3b 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,6 +1,7 @@
 using System;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
 {
@@ -28,10 +29,17 @@ namespace Jellyfin.Api.Helpers
                 : value.Split(separator);
         }
 
-        internal static SessionInfo GetSession(ISessionContext sessionContext)
+        internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
         {
-            // TODO: how do we get a SessionInfo without IRequest?
-            SessionInfo session = sessionContext.GetSession("Request");
+            var authorization = authContext.GetAuthorizationInfo(request);
+            var user = authorization.User;
+            var session = sessionManager.LogSessionActivity(
+                authorization.Client,
+                authorization.Version,
+                authorization.DeviceId,
+                authorization.Device,
+                request.HttpContext.Connection.RemoteIpAddress.ToString(),
+                user);
 
             if (session == null)
             {

From b51b9653ac9ff015d34233099bdc744fa153f8ee Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 14:29:32 +0200
Subject: [PATCH 206/463] Add missing authorization policies

---
 Jellyfin.Api/Controllers/SystemController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index cab6f308f0..f4dae40ef6 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
         [HttpGet("Info")]
-        // TODO: Authorize EscapeParentalControl
+        [Authorize(Policy = Policies.IgnoreSchedule)]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<SystemInfo>> GetSystemInfo()
@@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Server restarted.</response>
         /// <returns>No content. Server restarted.</returns>
         [HttpPost("Restart")]
-        // TODO: Authorize AllowLocal = true
+        [Authorize(Policy = Policies.LocalAccessOnly)]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RestartApplication()

From 08401f923dbe1bdf0086f905a6a9575b5ac9c5cd Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 15:49:44 +0200
Subject: [PATCH 207/463] Change swagger dictionary type mapping

---
 .../Extensions/ApiServiceCollectionExtensions.cs | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 821a52e476..aad61d0429 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -216,6 +216,9 @@ namespace Jellyfin.Server.Extensions
                         })
                 });
 
+            /*
+             * Support BlurHash dictionary
+             */
             options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
                 new OpenApiSchema
                 {
@@ -224,8 +227,17 @@ namespace Jellyfin.Server.Extensions
                         name => name,
                         name => new OpenApiSchema
                         {
-                            Type = "string",
-                            Format = "string"
+                            Type = "object", Properties = new Dictionary<string, OpenApiSchema>
+                            {
+                                {
+                                    "string",
+                                    new OpenApiSchema
+                                    {
+                                        Type = "string",
+                                        Format = "string"
+                                    }
+                                }
+                            }
                         })
                 });
         }

From d820c0fff50a79d85349660f507e820704d80d45 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 08:49:42 -0600
Subject: [PATCH 208/463] Convert pragma to supresswarning

---
 Jellyfin.Api/Controllers/ActivityLogController.cs    |  4 ++--
 Jellyfin.Api/Controllers/FilterController.cs         |  6 +++---
 Jellyfin.Api/Controllers/ItemRefreshController.cs    |  4 ++--
 .../Controllers/LibraryStructureController.cs        |  4 ++--
 Jellyfin.Api/Controllers/NotificationsController.cs  | 12 ++++++++++--
 Jellyfin.Api/Controllers/PluginsController.cs        |  6 +++---
 Jellyfin.Api/Controllers/SubtitleController.cs       |  5 ++---
 7 files changed, 24 insertions(+), 17 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 4ae7cf5069..ec50fb022e 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,6 +1,5 @@
-#pragma warning disable CA1801
-
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
@@ -41,6 +40,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
         [HttpGet("Entries")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 6a6e6a64a3..dc5b0d9061 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,5 @@
-#pragma warning disable CA1801
-
-using System;
+using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -137,6 +136,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Query filters.</returns>
         [HttpGet("/Items/Filters2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryFilters> GetQueryFilters(
             [FromQuery] Guid? userId,
             [FromQuery] string? parentId,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index a1df22e411..6a16a89c5a 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,6 +1,5 @@
-#pragma warning disable CA1801
-
 using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
@@ -54,6 +53,7 @@ namespace Jellyfin.Api.Controllers
         [Description("Refreshes metadata for an item.")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")]
         public ActionResult Post(
             [FromRoute] string id,
             [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index ca2905b114..62c5474099 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CA1801
-
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -56,6 +55,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
         {
             return _libraryManager.GetVirtualFolders(true);
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index a1f9b9e8f7..01dd23c77f 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CA1801
-
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Models.NotificationDtos;
@@ -45,6 +44,10 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
         [HttpGet("{UserID}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
         public ActionResult<NotificationResultDto> GetNotifications(
             [FromRoute] string userId,
             [FromQuery] bool? isRead,
@@ -62,6 +65,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
         [HttpGet("{UserID}/Summary")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
             [FromRoute] string userId)
         {
@@ -136,6 +140,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{UserID}/Read")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
         public ActionResult SetRead(
             [FromRoute] string userId,
             [FromQuery] string ids)
@@ -152,6 +158,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{UserID}/Unread")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
         public ActionResult SetUnread(
             [FromRoute] string userId,
             [FromQuery] string ids)
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index fdb2f4c35b..6075544cf7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CA1801
-
-using System;
+using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -46,6 +45,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Installed plugins returned.</response>
         /// <returns>List of currently installed plugins.</returns>
         [HttpGet]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
         {
             return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 69b83379d9..74ec5f9b52 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -1,8 +1,7 @@
-#pragma warning disable CA1801
-
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -251,9 +250,9 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> GetSubtitlePlaylist(
             [FromRoute] Guid id,
-            // TODO: 'int index' is never used: CA1801 is disabled
             [FromRoute] int index,
             [FromRoute] string mediaSourceId,
             [FromQuery, Required] int segmentLength)

From 97ce641242e9df8475e01dd65a30aaf2ec763ffd Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 09:00:50 -0600
Subject: [PATCH 209/463] remove #nullable

---
 Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 0c5fece832..2537996512 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;

From 6767c47ccdede27a374ea21b3013fe32ee356f01 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 09:01:37 -0600
Subject: [PATCH 210/463] remove #nullable

---
 Jellyfin.Api/Controllers/EnvironmentController.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 046ffdf8eb..719bb7d86d 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.IO;

From 8f1505cdf5bc964c294cf0575807233f0e7af7d7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 09:02:10 -0600
Subject: [PATCH 211/463] remove #nullable

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 6b42b500e0..a22cdd7803 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.Linq;

From e4a13f0e1e946ed306f2eb1be4217cde9d2622cb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 09:02:41 -0600
Subject: [PATCH 212/463] remove #nullable

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index e37e137d17..f7122c4134 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.Linq;

From d53a2899b1a82af1c64f3a2a558ae1ef201905ec Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 09:03:04 -0600
Subject: [PATCH 213/463] remove #nullable

---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 35efe6b5f8..697a0baf42 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System.ComponentModel.DataAnnotations;
 using System.Threading;
 using MediaBrowser.Controller.Persistence;

From 68ea589f1af5bc5fdc656013d6e5d1deec99341f Mon Sep 17 00:00:00 2001
From: David Ullmer <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 18:11:46 +0200
Subject: [PATCH 214/463] Use direct return instead of Ok()

---
 Jellyfin.Api/Controllers/UserController.cs | 22 ++++++++++------------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 24123085bf..68ab5813ce 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -1,9 +1,7 @@
-#nullable enable
-#pragma warning disable CA1801
-
-using System;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -76,6 +74,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<UserDto>> GetUsers(
             [FromQuery] bool? isHidden,
             [FromQuery] bool? isDisabled,
@@ -97,7 +96,7 @@ namespace Jellyfin.Api.Controllers
             // If the startup wizard hasn't been completed then just return all users
             if (!_config.Configuration.IsStartupWizardCompleted)
             {
-                return Ok(GetUsers(false, false, false).Value);
+                return Ok(Get(false, false, false, false));
             }
 
             return Ok(Get(false, false, true, true));
@@ -124,7 +123,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
-            return Ok(result);
+            return result;
         }
 
         /// <summary>
@@ -219,7 +218,7 @@ namespace Jellyfin.Api.Controllers
                     Username = request.Username
                 }).ConfigureAwait(false);
 
-                return Ok(result);
+                return result;
             }
             catch (SecurityException e)
             {
@@ -476,7 +475,7 @@ namespace Jellyfin.Api.Controllers
 
             var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
 
-            return Ok(result);
+            return result;
         }
 
         /// <summary>
@@ -494,7 +493,7 @@ namespace Jellyfin.Api.Controllers
 
             var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
 
-            return Ok(result);
+            return result;
         }
 
         /// <summary>
@@ -508,7 +507,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin)
         {
             var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
-            return Ok(result);
+            return result;
         }
 
         private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
@@ -545,8 +544,7 @@ namespace Jellyfin.Api.Controllers
 
             var result = users
                 .OrderBy(u => u.Username)
-                .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()))
-                .ToArray();
+                .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()));
 
             return result;
         }

From 7e91ded58792bb052ced4705cac08747ca2ea9d8 Mon Sep 17 00:00:00 2001
From: David Ullmer <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 18:20:49 +0200
Subject: [PATCH 215/463] Remove #nullable enable

---
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs    | 4 +---
 Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 2 --
 Jellyfin.Api/Helpers/ClaimHelpers.cs             | 4 +---
 3 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index b5b9d89041..953acac807 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System.Net;
+using System.Net;
 using System.Security.Claims;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Enums;
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 5e5e25e847..ea02e6a0b1 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
 using System.Globalization;
 using System.Security.Authentication;
 using System.Security.Claims;
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index a07d4ed820..df235ced25 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System;
+using System;
 using System.Linq;
 using System.Security.Claims;
 using Jellyfin.Api.Constants;

From e2a7e8d97e26059d034e7c338adc0eb191642d80 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 13:10:10 -0600
Subject: [PATCH 216/463] Move LibraryService.cs to Jellyfin.Api

---
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs |   11 +-
 .../Auth/DownloadPolicy/DownloadHandler.cs    |   45 +
 .../DownloadPolicy/DownloadRequirement.cs     |   11 +
 Jellyfin.Api/Constants/Policies.cs            |    5 +
 Jellyfin.Api/Controllers/LibraryController.cs |  965 +++++++++++++-
 Jellyfin.Api/Helpers/RequestHelpers.cs        |   18 +
 .../LibraryDtos/LibraryOptionInfoDto.cs       |   18 +
 .../LibraryDtos/LibraryOptionsResultDto.cs    |   34 +
 .../LibraryDtos/LibraryTypeOptionsDto.cs      |   41 +
 .../Models/LibraryDtos/MediaUpdateInfoDto.cs  |   19 +
 .../ApiServiceCollectionExtensions.cs         |    9 +
 MediaBrowser.Api/Library/LibraryService.cs    | 1116 -----------------
 MediaBrowser.Api/MediaBrowser.Api.csproj      |    4 +
 13 files changed, 1178 insertions(+), 1118 deletions(-)
 create mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
 create mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
 create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
 create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
 create mode 100644 Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
 create mode 100644 Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
 delete mode 100644 MediaBrowser.Api/Library/LibraryService.cs

diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index b5b9d89041..c66b841fae 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -44,11 +44,13 @@ namespace Jellyfin.Api.Auth
         /// <param name="claimsPrincipal">Request claims.</param>
         /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
         /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
+        /// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
         /// <returns>Validated claim status.</returns>
         protected bool ValidateClaims(
             ClaimsPrincipal claimsPrincipal,
             bool ignoreSchedule = false,
-            bool localAccessOnly = false)
+            bool localAccessOnly = false,
+            bool requiredDownloadPermission = false)
         {
             // Ensure claim has userId.
             var userId = ClaimHelpers.GetUserId(claimsPrincipal);
@@ -91,6 +93,13 @@ namespace Jellyfin.Api.Auth
                 return false;
             }
 
+            // User attempting to download without permission.
+            if (requiredDownloadPermission
+                && !user.HasPermission(PermissionKind.EnableContentDownloading))
+            {
+                return false;
+            }
+
             return true;
         }
 
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
new file mode 100644
index 0000000000..fcfa55dfec
--- /dev/null
+++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
@@ -0,0 +1,45 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.DownloadPolicy
+{
+    /// <summary>
+    /// Download authorization handler.
+    /// </summary>
+    public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DownloadHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public DownloadHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User);
+            if (validated)
+            {
+                context.Succeed(requirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
new file mode 100644
index 0000000000..b0a72a9dec
--- /dev/null
+++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.DownloadPolicy
+{
+    /// <summary>
+    /// The download permission requirement.
+    /// </summary>
+    public class DownloadRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index cf574e43df..851b56d732 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -29,5 +29,10 @@ namespace Jellyfin.Api.Constants
         /// Policy name for escaping schedule controls.
         /// </summary>
         public const string IgnoreSchedule = "IgnoreSchedule";
+
+        /// <summary>
+        /// Policy name for requiring download permission.
+        /// </summary>
+        public const string Download = "Download";
     }
 }
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index f45101c0cb..e3e2e94894 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -1,10 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.LibraryDtos;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Movie = Jellyfin.Data.Entities.Movie;
+using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -21,6 +52,8 @@ namespace Jellyfin.Api.Controllers
         private readonly IActivityManager _activityManager;
         private readonly ILocalizationManager _localization;
         private readonly ILibraryMonitor _libraryMonitor;
+        private readonly ILogger<LibraryController> _logger;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryController"/> class.
@@ -33,6 +66,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
         /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         public LibraryController(
             IProviderManager providerManager,
             ILibraryManager libraryManager,
@@ -41,7 +76,9 @@ namespace Jellyfin.Api.Controllers
             IAuthorizationContext authContext,
             IActivityManager activityManager,
             ILocalizationManager localization,
-            ILibraryMonitor libraryMonitor)
+            ILibraryMonitor libraryMonitor,
+            ILogger<LibraryController> logger,
+            IServerConfigurationManager serverConfigurationManager)
         {
             _providerManager = providerManager;
             _libraryManager = libraryManager;
@@ -51,6 +88,932 @@ namespace Jellyfin.Api.Controllers
             _activityManager = activityManager;
             _localization = localization;
             _libraryMonitor = libraryMonitor;
+            _logger = logger;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Get the original file of an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="200">File stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
+        [HttpGet("/Items/{itemId}/File")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult GetFile([FromRoute] Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read);
+            return File(fileStream, MimeTypes.GetMimeType(item.Path));
+        }
+
+        /// <summary>
+        /// Gets critic review for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <response code="200">Critic reviews returned.</response>
+        /// <returns>The list of critic reviews.</returns>
+        [HttpGet("/Items/{itemId}/CriticReviews")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Obsolete("This endpoint is obsolete.")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
+        public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews(
+            [FromRoute] Guid itemId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit)
+        {
+            return new QueryResult<BaseItemDto>();
+        }
+
+        /// <summary>
+        /// Get theme songs for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+        /// <response code="200">Theme songs returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>The item theme songs.</returns>
+        [HttpGet("/Items/{itemId}/ThemeSongs")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<ThemeMediaResult> GetThemeSongs(
+            [FromRoute] Guid itemId,
+            [FromQuery] Guid userId,
+            [FromQuery] bool inheritFromParent)
+        {
+            var user = !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId)
+                : null;
+
+            var item = itemId.Equals(Guid.Empty)
+                ? (!userId.Equals(Guid.Empty)
+                    ? _libraryManager.GetUserRootFolder()
+                    : _libraryManager.RootFolder)
+                : _libraryManager.GetItemById(itemId);
+
+            if (item == null)
+            {
+                return NotFound("Item not found.");
+            }
+
+            IEnumerable<BaseItem> themeItems;
+
+            while (true)
+            {
+                themeItems = item.GetThemeSongs();
+
+                if (themeItems.Any() || !inheritFromParent)
+                {
+                    break;
+                }
+
+                var parent = item.GetParent();
+                if (parent == null)
+                {
+                    break;
+                }
+
+                item = parent;
+            }
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var items = themeItems
+                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+                .ToArray();
+
+            return new ThemeMediaResult
+            {
+                Items = items,
+                TotalRecordCount = items.Length,
+                OwnerId = item.Id
+            };
+        }
+
+        /// <summary>
+        /// Get theme videos for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+        /// <response code="200">Theme videos returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>The item theme videos.</returns>
+        [HttpGet("/Items/{itemId}/ThemeVideos")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<ThemeMediaResult> GetThemeVideos(
+            [FromRoute] Guid itemId,
+            [FromQuery] Guid userId,
+            [FromQuery] bool inheritFromParent)
+        {
+            var user = !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId)
+                : null;
+
+            var item = itemId.Equals(Guid.Empty)
+                ? (!userId.Equals(Guid.Empty)
+                    ? _libraryManager.GetUserRootFolder()
+                    : _libraryManager.RootFolder)
+                : _libraryManager.GetItemById(itemId);
+
+            if (item == null)
+            {
+                return NotFound("Item not found.");
+            }
+
+            IEnumerable<BaseItem> themeItems;
+
+            while (true)
+            {
+                themeItems = item.GetThemeVideos();
+
+                if (themeItems.Any() || !inheritFromParent)
+                {
+                    break;
+                }
+
+                var parent = item.GetParent();
+                if (parent == null)
+                {
+                    break;
+                }
+
+                item = parent;
+            }
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var items = themeItems
+                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+                .ToArray();
+
+            return new ThemeMediaResult
+            {
+                Items = items,
+                TotalRecordCount = items.Length,
+                OwnerId = item.Id
+            };
+        }
+
+        /// <summary>
+        /// Get theme songs and videos for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+        /// <response code="200">Theme songs and videos returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>The item theme videos.</returns>
+        public ActionResult<AllThemeMediaResult> GetThemeMedia(
+            [FromRoute] Guid itemId,
+            [FromQuery] Guid userId,
+            [FromQuery] bool inheritFromParent)
+        {
+            var themeSongs = GetThemeSongs(
+                itemId,
+                userId,
+                inheritFromParent);
+
+            var themeVideos = GetThemeVideos(
+                itemId,
+                userId,
+                inheritFromParent);
+
+            return new AllThemeMediaResult
+            {
+                ThemeSongsResult = themeSongs?.Value,
+                ThemeVideosResult = themeVideos?.Value,
+                SoundtrackSongsResult = new ThemeMediaResult()
+            };
+        }
+
+        /// <summary>
+        /// Starts a library scan.
+        /// </summary>
+        /// <response code="204">Library scan started.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpGet("/Library/Refresh")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public async Task<ActionResult> RefreshLibrary()
+        {
+            try
+            {
+                await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                 _logger.LogError(ex, "Error refreshing library");
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Deletes an item from the library and filesystem.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="204">Item deleted.</response>
+        /// <response code="401">Unauthorized access.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Items/{itemId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult DeleteItem(Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            var auth = _authContext.GetAuthorizationInfo(Request);
+            var user = auth.User;
+
+            if (!item.CanDelete(user))
+            {
+                return Unauthorized("Unauthorized access");
+            }
+
+            _libraryManager.DeleteItem(
+                item,
+                new DeleteOptions { DeleteFileLocation = true },
+                true);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Deletes items from the library and filesystem.
+        /// </summary>
+        /// <param name="ids">The item ids.</param>
+        /// <response code="204">Items deleted.</response>
+        /// <response code="401">Unauthorized access.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Items")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult DeleteItems([FromQuery] string ids)
+        {
+            var itemIds = string.IsNullOrWhiteSpace(ids)
+                ? Array.Empty<string>()
+                : RequestHelpers.Split(ids, ',', true);
+
+            foreach (var i in itemIds)
+            {
+                var item = _libraryManager.GetItemById(i);
+                var auth = _authContext.GetAuthorizationInfo(Request);
+                var user = auth.User;
+
+                if (!item.CanDelete(user))
+                {
+                    if (ids.Length > 1)
+                    {
+                        return Unauthorized("Unauthorized access");
+                    }
+
+                    continue;
+                }
+
+                _libraryManager.DeleteItem(
+                    item,
+                    new DeleteOptions { DeleteFileLocation = true },
+                    true);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Get item counts.
+        /// </summary>
+        /// <param name="userId">Optional. Get counts from a specific user's library.</param>
+        /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
+        /// <response code="200">Item counts returned.</response>
+        /// <returns>Item counts.</returns>
+        [HttpGet("/Items/Counts")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<ItemCounts> GetItemCounts(
+            [FromQuery] Guid userId,
+            [FromQuery] bool? isFavorite)
+        {
+            var user = userId.Equals(Guid.Empty)
+                ? null
+                : _userManager.GetUserById(userId);
+
+            var counts = new ItemCounts
+            {
+                AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite),
+                EpisodeCount = GetCount(typeof(Episode), user, isFavorite),
+                MovieCount = GetCount(typeof(Movie), user, isFavorite),
+                SeriesCount = GetCount(typeof(Series), user, isFavorite),
+                SongCount = GetCount(typeof(Audio), user, isFavorite),
+                MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite),
+                BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite),
+                BookCount = GetCount(typeof(Book), user, isFavorite)
+            };
+
+            return counts;
+        }
+
+        /// <summary>
+        /// Gets all parents of an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Item parents returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>Item parents.</returns>
+        [HttpGet("/Items/{itemId}/Ancestors")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+
+            if (item == null)
+            {
+                return NotFound("Item not found");
+            }
+
+            var baseItemDtos = new List<BaseItemDto>();
+
+            var user = !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId)
+                : null;
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            BaseItem parent = item.GetParent();
+
+            while (parent != null)
+            {
+                if (user != null)
+                {
+                    parent = TranslateParentItem(parent, user);
+                }
+
+                baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
+
+                parent = parent.GetParent();
+            }
+
+            return baseItemDtos;
+        }
+
+        /// <summary>
+        /// Gets a list of physical paths from virtual folders.
+        /// </summary>
+        /// <response code="200">Physical paths returned.</response>
+        /// <returns>List of physical paths.</returns>
+        [HttpGet("/Library/PhysicalPaths")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        public ActionResult<IEnumerable<string>> GetPhysicalPaths()
+        {
+            return Ok(_libraryManager.RootFolder.Children
+                .SelectMany(c => c.PhysicalLocations));
+        }
+
+        /// <summary>
+        /// Gets all user media folders.
+        /// </summary>
+        /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
+        /// <returns>List of user media folders.</returns>
+        [HttpGet("/Library/MediaFolders")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
+        {
+            var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+
+            if (isHidden.HasValue)
+            {
+                var val = isHidden.Value;
+
+                items = items.Where(i => i.IsHidden == val).ToList();
+            }
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var result = new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = items.Count,
+                Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray()
+            };
+
+            return result;
+        }
+
+        /// <summary>
+        /// Reports that new episodes of a series have been added by an external source.
+        /// </summary>
+        /// <param name="tvdbId">The tvdbId.</param>
+        /// <response code="204">Report success.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Library/Series/Added")]
+        [HttpPost("/Library/Series/Updated")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult PostUpdatedSeries([FromQuery] string tvdbId)
+        {
+            var series = _libraryManager.GetItemList(new InternalItemsQuery
+            {
+                IncludeItemTypes = new[] { nameof(Series) },
+                DtoOptions = new DtoOptions(false)
+                {
+                    EnableImages = false
+                }
+            }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
+
+            foreach (var item in series)
+            {
+                _libraryMonitor.ReportFileSystemChanged(item.Path);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that new movies have been added by an external source.
+        /// </summary>
+        /// <param name="tmdbId">The tmdbId.</param>
+        /// <param name="imdbId">The imdbId.</param>
+        /// <response code="204">Report success.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Library/Movies/Added")]
+        [HttpPost("/Library/Movies/Updated")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult PostUpdatedMovies([FromRoute] string tmdbId, [FromRoute] string imdbId)
+        {
+            var movies = _libraryManager.GetItemList(new InternalItemsQuery
+            {
+                IncludeItemTypes = new[] { nameof(Movie) },
+                DtoOptions = new DtoOptions(false)
+                {
+                    EnableImages = false
+                }
+            });
+
+            if (!string.IsNullOrWhiteSpace(imdbId))
+            {
+                movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+            else if (!string.IsNullOrWhiteSpace(tmdbId))
+            {
+                movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+            else
+            {
+                movies = new List<BaseItem>();
+            }
+
+            foreach (var item in movies)
+            {
+                _libraryMonitor.ReportFileSystemChanged(item.Path);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that new movies have been added by an external source.
+        /// </summary>
+        /// <param name="updates">A list of updated media paths.</param>
+        /// <response code="204">Report success.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Library/Media/Updated")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates)
+        {
+            foreach (var item in updates)
+            {
+                _libraryMonitor.ReportFileSystemChanged(item.Path);
+            }
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Downloads item media.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="200">Media downloaded.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
+        /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
+        [HttpGet("/Items/{itemId}/Download")]
+        [Authorize(Policy = Policies.Download)]
+        public ActionResult GetDownload([FromRoute] Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            var auth = _authContext.GetAuthorizationInfo(Request);
+
+            var user = auth.User;
+
+            if (user != null)
+            {
+                if (!item.CanDownload(user))
+                {
+                    throw new ArgumentException("Item does not support downloading");
+                }
+            }
+            else
+            {
+                if (!item.CanDownload())
+                {
+                    throw new ArgumentException("Item does not support downloading");
+                }
+            }
+
+            if (user != null)
+            {
+                LogDownload(item, user, auth);
+            }
+
+            var path = item.Path;
+
+            // Quotes are valid in linux. They'll possibly cause issues here
+            var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty, StringComparison.Ordinal);
+            if (!string.IsNullOrWhiteSpace(filename))
+            {
+                // Kestrel doesn't support non-ASCII characters in headers
+                if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
+                {
+                    // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
+                    filename = WebUtility.UrlEncode(filename);
+                }
+            }
+
+            // TODO determine non-ASCII validity.
+            using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
+            return File(fileStream, MimeTypes.GetMimeType(path), filename);
+        }
+
+        /// <summary>
+        /// Gets similar items.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="excludeArtistIds">Exclude artist ids.</param>
+        /// <param name="enableImages">(Unused) Optional. include image information in output.</param>
+        /// <param name="enableUserData">(Unused) Optional. include user data.</param>
+        /// <param name="imageTypeLimit">(Unused) Optional. the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">(Unused) Optional. The image types to include in the output.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
+        [HttpGet("/Artists/{itemId}/Similar")]
+        [HttpGet("/Items/{itemId}/Similar")]
+        [HttpGet("/Albums/{itemId}/Similar")]
+        [HttpGet("/Shows/{itemId}/Similar")]
+        [HttpGet("/Movies/{itemId}/Similar")]
+        [HttpGet("/Trailers/{itemId}/Similar")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+        public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+            [FromRoute] Guid itemId,
+            [FromQuery] string excludeArtistIds,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields)
+        {
+            var item = itemId.Equals(Guid.Empty)
+                ? (!userId.Equals(Guid.Empty)
+                    ? _libraryManager.GetUserRootFolder()
+                    : _libraryManager.RootFolder)
+                : _libraryManager.GetItemById(itemId);
+
+            var program = item as IHasProgramAttributes;
+            if (item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer)
+            {
+                /*
+                 * // TODO
+                return new MoviesService(
+                    _moviesServiceLogger,
+                    ServerConfigurationManager,
+                    ResultFactory,
+                    _userManager,
+                    _libraryManager,
+                    _dtoService,
+                    _authContext)
+                {
+                    Request = Request,
+                }.GetSimilarItemsResult(request);*/
+            }
+
+            if (program != null && program.IsSeries)
+            {
+                return GetSimilarItemsResult(
+                    item,
+                    excludeArtistIds,
+                    userId,
+                    limit,
+                    fields,
+                    new[] { nameof(Series) });
+            }
+
+            if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
+            {
+                return new QueryResult<BaseItemDto>();
+            }
+
+            return GetSimilarItemsResult(
+                item,
+                excludeArtistIds,
+                userId,
+                limit,
+                fields,
+                new[] { item.GetType().Name });
+        }
+
+        /// <summary>
+        /// Gets the library options info.
+        /// </summary>
+        /// <param name="libraryContentType">Library content type.</param>
+        /// <param name="isNewLibrary">Whether this is a new library.</param>
+        /// <response code="200">Library options info returned.</response>
+        /// <returns>Library options info.</returns>
+        [HttpGet("/Libraries/AvailableOptions")]
+        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo([FromQuery] string libraryContentType, [FromQuery] bool isNewLibrary)
+        {
+            var result = new LibraryOptionsResultDto();
+
+            var types = GetRepresentativeItemTypes(libraryContentType);
+            var typesList = types.ToList();
+
+            var plugins = _providerManager.GetAllMetadataPlugins()
+                .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
+                .OrderBy(i => typesList.IndexOf(i.ItemType))
+                .ToList();
+
+            result.MetadataSavers = plugins
+                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
+                .Select(i => new LibraryOptionInfoDto
+                {
+                    Name = i.Name,
+                    DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
+                })
+                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+                .Select(x => x.First())
+                .ToArray();
+
+            result.MetadataReaders = plugins
+                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
+                .Select(i => new LibraryOptionInfoDto
+                {
+                    Name = i.Name,
+                    DefaultEnabled = true
+                })
+                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+                .Select(x => x.First())
+                .ToArray();
+
+            result.SubtitleFetchers = plugins
+                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
+                .Select(i => new LibraryOptionInfoDto
+                {
+                    Name = i.Name,
+                    DefaultEnabled = true
+                })
+                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+                .Select(x => x.First())
+                .ToArray();
+
+            var typeOptions = new List<LibraryTypeOptionsDto>();
+
+            foreach (var type in types)
+            {
+                TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+
+                typeOptions.Add(new LibraryTypeOptionsDto
+                {
+                    Type = type,
+
+                    MetadataFetchers = plugins
+                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
+                    .Select(i => new LibraryOptionInfoDto
+                    {
+                        Name = i.Name,
+                        DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+                    })
+                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+                    .Select(x => x.First())
+                    .ToArray(),
+
+                    ImageFetchers = plugins
+                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
+                    .Select(i => new LibraryOptionInfoDto
+                    {
+                        Name = i.Name,
+                        DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+                    })
+                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+                    .Select(x => x.First())
+                    .ToArray(),
+
+                    SupportedImageTypes = plugins
+                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+                    .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
+                    .Distinct()
+                    .ToArray(),
+
+                    DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
+                });
+            }
+
+            result.TypeOptions = typeOptions.ToArray();
+
+            return result;
+        }
+
+        private int GetCount(Type type, User? user, bool? isFavorite)
+        {
+            var query = new InternalItemsQuery(user)
+            {
+                IncludeItemTypes = new[] { type.Name },
+                Limit = 0,
+                Recursive = true,
+                IsVirtualItem = false,
+                IsFavorite = isFavorite,
+                DtoOptions = new DtoOptions(false)
+                {
+                    EnableImages = false
+                }
+            };
+
+            return _libraryManager.GetItemsResult(query).TotalRecordCount;
+        }
+
+        private BaseItem TranslateParentItem(BaseItem item, User user)
+        {
+            return item.GetParent() is AggregateFolder
+                ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
+                    .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
+                : item;
+        }
+
+        private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
+        {
+            try
+            {
+                _activityManager.Create(new ActivityLog(
+                    string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+                    "UserDownloadingContent",
+                    auth.UserId)
+                {
+                    ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
+                });
+            }
+            catch
+            {
+                // Logged at lower levels
+            }
+        }
+
+        private QueryResult<BaseItemDto> GetSimilarItemsResult(
+            BaseItem item,
+            string excludeArtistIds,
+            Guid userId,
+            int? limit,
+            string fields,
+            string[] includeItemTypes)
+        {
+            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request);
+
+            var query = new InternalItemsQuery(user)
+            {
+                Limit = limit,
+                IncludeItemTypes = includeItemTypes,
+                SimilarTo = item,
+                DtoOptions = dtoOptions,
+                EnableTotalRecordCount = false
+            };
+
+            // ExcludeArtistIds
+            if (!string.IsNullOrEmpty(excludeArtistIds))
+            {
+                query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+            }
+
+            List<BaseItem> itemsResult;
+
+            if (item is MusicArtist)
+            {
+                query.IncludeItemTypes = Array.Empty<string>();
+
+                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
+            }
+            else
+            {
+                itemsResult = _libraryManager.GetItemList(query);
+            }
+
+            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+
+            var result = new QueryResult<BaseItemDto>
+            {
+                Items = returnList,
+                TotalRecordCount = itemsResult.Count
+            };
+
+            return result;
+        }
+
+        private static string[] GetRepresentativeItemTypes(string contentType)
+        {
+            return contentType switch
+            {
+                CollectionType.BoxSets => new[] { "BoxSet" },
+                CollectionType.Playlists => new[] { "Playlist" },
+                CollectionType.Movies => new[] { "Movie" },
+                CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
+                CollectionType.Books => new[] { "Book" },
+                CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
+                CollectionType.HomeVideos => new[] { "Video", "Photo" },
+                CollectionType.Photos => new[] { "Video", "Photo" },
+                CollectionType.MusicVideos => new[] { "MusicVideo" },
+                _ => new[] { "Series", "Season", "Episode", "Movie" }
+            };
+        }
+
+        private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
+        {
+            if (isNewLibrary)
+            {
+                return false;
+            }
+
+            var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+                .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                .ToArray();
+
+            return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
+        }
+
+        private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+        {
+            if (isNewLibrary)
+            {
+                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
+                {
+                    return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+                         || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+                         || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
+                }
+
+                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+                   || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+                   || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
+            }
+
+            var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+                .ToArray();
+
+            return metadataOptions.Length == 0
+               || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+        }
+
+        private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+        {
+            if (isNewLibrary)
+            {
+                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
+                {
+                    return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
+                           && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+                           && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+                           && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
+                }
+
+                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+                       || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
+                       || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+                       || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+            }
+
+            var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+                .ToArray();
+
+            if (metadataOptions.Length == 0)
+            {
+                return true;
+            }
+
+            return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
         }
     }
 }
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 9f4d34f9c6..a57cf146f6 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 
 namespace Jellyfin.Api.Helpers
 {
@@ -25,5 +26,22 @@ namespace Jellyfin.Api.Helpers
                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
                 : value.Split(separator);
         }
+
+        /// <summary>
+        /// Splits a comma delimited string and parses Guids.
+        /// </summary>
+        /// <param name="value">Input value.</param>
+        /// <returns>Parsed Guids.</returns>
+        public static Guid[] GetGuids(string value)
+        {
+            if (value == null)
+            {
+                return Array.Empty<Guid>();
+            }
+
+            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(i => new Guid(i))
+                .ToArray();
+        }
     }
 }
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
new file mode 100644
index 0000000000..3584344344
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionInfoDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// Library option info dto.
+    /// </summary>
+    public class LibraryOptionInfoDto
+    {
+        /// <summary>
+        /// Gets or sets name.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether default enabled.
+        /// </summary>
+        public bool DefaultEnabled { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
new file mode 100644
index 0000000000..33eda33cb9
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// Library options result dto.
+    /// </summary>
+    public class LibraryOptionsResultDto
+    {
+        /// <summary>
+        /// Gets or sets the metadata savers.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")]
+        public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the metadata readers.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")]
+        public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the subtitle fetchers.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")]
+        public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the type options.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")]
+        public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!;
+    }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
new file mode 100644
index 0000000000..ad031e95e5
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// Library type options dto.
+    /// </summary>
+    public class LibraryTypeOptionsDto
+    {
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        public string? Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the metadata fetchers.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")]
+        public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the image fetchers.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")]
+        public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the supported image types.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")]
+        public ImageType[] SupportedImageTypes { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets the default image options.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")]
+        public ImageOption[] DefaultImageOptions { get; set; } = null!;
+    }
+}
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
new file mode 100644
index 0000000000..991dbfc502
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// Media Update Info Dto.
+    /// </summary>
+    public class MediaUpdateInfoDto
+    {
+        /// <summary>
+        /// Gets or sets media path.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets media update type.
+        /// Created, Modified, Deleted.
+        /// </summary>
+        public string? UpdateType { get; set; }
+    }
+}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index dbd5ba4166..5f2fb7ea64 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -6,6 +6,7 @@ using System.Reflection;
 using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Auth.DownloadPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
@@ -39,6 +40,7 @@ namespace Jellyfin.Server.Extensions
         public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
         {
             serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
@@ -52,6 +54,13 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new DefaultAuthorizationRequirement());
                     });
+                options.AddPolicy(
+                    Policies.Download,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new DownloadRequirement());
+                    });
                 options.AddPolicy(
                     Policies.FirstTimeSetupOrElevated,
                     policy =>
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
deleted file mode 100644
index e96875403d..0000000000
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ /dev/null
@@ -1,1116 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Api.Movies;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-
-namespace MediaBrowser.Api.Library
-{
-    [Route("/Items/{Id}/File", "GET", Summary = "Gets the original file of an item")]
-    [Authenticated]
-    public class GetFile
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetCriticReviews
-    /// </summary>
-    [Route("/Items/{Id}/CriticReviews", "GET", Summary = "Gets critic reviews for an item")]
-    [Authenticated]
-    public class GetCriticReviews : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeSongs
-    /// </summary>
-    [Route("/Items/{Id}/ThemeSongs", "GET", Summary = "Gets theme songs for an item")]
-    [Authenticated]
-    public class GetThemeSongs : IReturn<ThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeVideos
-    /// </summary>
-    [Route("/Items/{Id}/ThemeVideos", "GET", Summary = "Gets theme videos for an item")]
-    [Authenticated]
-    public class GetThemeVideos : IReturn<ThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeVideos
-    /// </summary>
-    [Route("/Items/{Id}/ThemeMedia", "GET", Summary = "Gets theme videos and songs for an item")]
-    [Authenticated]
-    public class GetThemeMedia : IReturn<AllThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    [Route("/Library/Refresh", "POST", Summary = "Starts a library scan")]
-    [Authenticated(Roles = "Admin")]
-    public class RefreshLibrary : IReturnVoid
-    {
-    }
-
-    [Route("/Items/{Id}", "DELETE", Summary = "Deletes an item from the library and file system")]
-    [Authenticated]
-    public class DeleteItem : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items", "DELETE", Summary = "Deletes an item from the library and file system")]
-    [Authenticated]
-    public class DeleteItems : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Ids", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Ids { get; set; }
-    }
-
-    [Route("/Items/Counts", "GET")]
-    [Authenticated]
-    public class GetItemCounts : IReturn<ItemCounts>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional. Get counts from a specific user's library.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "IsFavorite", Description = "Optional. Get counts of favorite items", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-    }
-
-    [Route("/Items/{Id}/Ancestors", "GET", Summary = "Gets all parents of an item")]
-    [Authenticated]
-    public class GetAncestors : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPhyscialPaths
-    /// </summary>
-    [Route("/Library/PhysicalPaths", "GET", Summary = "Gets a list of physical paths from virtual folders")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPhyscialPaths : IReturn<List<string>>
-    {
-    }
-
-    [Route("/Library/MediaFolders", "GET", Summary = "Gets all user media folders.")]
-    [Authenticated]
-    public class GetMediaFolders : IReturn<QueryResult<BaseItemDto>>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-    }
-
-    [Route("/Library/Series/Added", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
-    [Route("/Library/Series/Updated", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedSeries : IReturnVoid
-    {
-        [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string TvdbId { get; set; }
-    }
-
-    [Route("/Library/Movies/Added", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Route("/Library/Movies/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedMovies : IReturnVoid
-    {
-        [ApiMember(Name = "TmdbId", Description = "Tmdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string TmdbId { get; set; }
-        [ApiMember(Name = "ImdbId", Description = "Imdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string ImdbId { get; set; }
-    }
-
-    public class MediaUpdateInfo
-    {
-        public string Path { get; set; }
-
-        // Created, Modified, Deleted
-        public string UpdateType { get; set; }
-    }
-
-    [Route("/Library/Media/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedMedia : IReturnVoid
-    {
-        [ApiMember(Name = "Updates", Description = "A list of updated media paths", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public List<MediaUpdateInfo> Updates { get; set; }
-    }
-
-    [Route("/Items/{Id}/Download", "GET", Summary = "Downloads item media")]
-    [Authenticated(Roles = "download")]
-    public class GetDownload
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    [Route("/Items/{Id}/Similar", "GET", Summary = "Gets similar items")]
-    [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")]
-    [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")]
-    [Route("/Trailers/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
-    [Authenticated]
-    public class GetSimilarItems : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Libraries/AvailableOptions", "GET")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetLibraryOptionsInfo : IReturn<LibraryOptionsResult>
-    {
-        public string LibraryContentType { get; set; }
-        public bool IsNewLibrary { get; set; }
-    }
-
-    public class LibraryOptionInfo
-    {
-        public string Name { get; set; }
-        public bool DefaultEnabled { get; set; }
-    }
-
-    public class LibraryOptionsResult
-    {
-        public LibraryOptionInfo[] MetadataSavers { get; set; }
-        public LibraryOptionInfo[] MetadataReaders { get; set; }
-        public LibraryOptionInfo[] SubtitleFetchers { get; set; }
-        public LibraryTypeOptions[] TypeOptions { get; set; }
-    }
-
-    public class LibraryTypeOptions
-    {
-        public string Type { get; set; }
-        public LibraryOptionInfo[] MetadataFetchers { get; set; }
-        public LibraryOptionInfo[] ImageFetchers { get; set; }
-        public ImageType[] SupportedImageTypes { get; set; }
-        public ImageOption[] DefaultImageOptions { get; set; }
-    }
-
-    /// <summary>
-    /// Class LibraryService
-    /// </summary>
-    public class LibraryService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserManager _userManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IActivityManager _activityManager;
-        private readonly ILocalizationManager _localization;
-        private readonly ILibraryMonitor _libraryMonitor;
-
-        private readonly ILogger<MoviesService> _moviesServiceLogger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryService" /> class.
-        /// </summary>
-        public LibraryService(
-            ILogger<LibraryService> logger,
-            ILogger<MoviesService> moviesServiceLogger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            ILibraryManager libraryManager,
-            IUserManager userManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            IActivityManager activityManager,
-            ILocalizationManager localization,
-            ILibraryMonitor libraryMonitor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _activityManager = activityManager;
-            _localization = localization;
-            _libraryMonitor = libraryMonitor;
-            _moviesServiceLogger = moviesServiceLogger;
-        }
-
-        // Content Types available for each Library
-        private string[] GetRepresentativeItemTypes(string contentType)
-        {
-            return contentType switch
-            {
-                CollectionType.BoxSets => new[] {"BoxSet"},
-                CollectionType.Playlists => new[] {"Playlist"},
-                CollectionType.Movies => new[] {"Movie"},
-                CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
-                CollectionType.Books => new[] {"Book"},
-                CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
-                CollectionType.HomeVideos => new[] {"Video", "Photo"},
-                CollectionType.Photos => new[] {"Video", "Photo"},
-                CollectionType.MusicVideos => new[] {"MusicVideo"},
-                _ => new[] {"Series", "Season", "Episode", "Movie"}
-            };
-        }
-
-        private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                return false;
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
-                .ToArray();
-
-            if (metadataOptions.Length == 0)
-            {
-                return true;
-            }
-
-            return metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
-                {
-                    return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
-                         || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
-                         || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
-                }
-
-                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
-                   || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
-                   || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                .ToArray();
-
-            return metadataOptions.Length == 0
-               || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
-                {
-                    return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
-                }
-
-                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                .ToArray();
-
-            if (metadataOptions.Length == 0)
-            {
-                return true;
-            }
-
-            return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        public object Get(GetLibraryOptionsInfo request)
-        {
-            var result = new LibraryOptionsResult();
-
-            var types = GetRepresentativeItemTypes(request.LibraryContentType);
-            var isNewLibrary = request.IsNewLibrary;
-            var typesList = types.ToList();
-
-            var plugins = _providerManager.GetAllMetadataPlugins()
-                .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
-                .OrderBy(i => typesList.IndexOf(i.ItemType))
-                .ToList();
-
-            result.MetadataSavers = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            result.MetadataReaders = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = true
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            result.SubtitleFetchers = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = true
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            var typeOptions = new List<LibraryTypeOptions>();
-
-            foreach (var type in types)
-            {
-                TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
-
-                typeOptions.Add(new LibraryTypeOptions
-                {
-                    Type = type,
-
-                    MetadataFetchers = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
-                    .Select(i => new LibraryOptionInfo
-                    {
-                        Name = i.Name,
-                        DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
-                    })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
-                    .ToArray(),
-
-                    ImageFetchers = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
-                    .Select(i => new LibraryOptionInfo
-                    {
-                        Name = i.Name,
-                        DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
-                    })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
-                    .ToArray(),
-
-                    SupportedImageTypes = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
-                    .Distinct()
-                    .ToArray(),
-
-                    DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
-                });
-            }
-
-            result.TypeOptions = typeOptions.ToArray();
-
-            return result;
-        }
-
-        public object Get(GetSimilarItems request)
-        {
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var program = item as IHasProgramAttributes;
-
-            if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
-            {
-                return new MoviesService(
-                    _moviesServiceLogger,
-                    ServerConfigurationManager,
-                    ResultFactory,
-                    _userManager,
-                    _libraryManager,
-                    _dtoService,
-                    _authContext)
-                {
-                    Request = Request,
-
-                }.GetSimilarItemsResult(request);
-            }
-
-            if (program != null && program.IsSeries)
-            {
-                return GetSimilarItemsResult(request, new[] { typeof(Series).Name });
-            }
-
-            if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
-            {
-                return new QueryResult<BaseItemDto>();
-            }
-
-            return GetSimilarItemsResult(request, new[] { item.GetType().Name });
-        }
-
-        private QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, string[] includeItemTypes)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                IncludeItemTypes = includeItemTypes,
-                SimilarTo = item,
-                DtoOptions = dtoOptions,
-                EnableTotalRecordCount = false
-            };
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
-            {
-                query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
-            }
-
-            List<BaseItem> itemsResult;
-
-            if (item is MusicArtist)
-            {
-                query.IncludeItemTypes = Array.Empty<string>();
-
-                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
-            }
-            else
-            {
-                itemsResult = _libraryManager.GetItemList(query);
-            }
-
-            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnList,
-                TotalRecordCount = itemsResult.Count
-            };
-
-            return result;
-        }
-
-        public object Get(GetMediaFolders request)
-        {
-            var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
-
-            if (request.IsHidden.HasValue)
-            {
-                var val = request.IsHidden.Value;
-
-                items = items.Where(i => i.IsHidden == val).ToList();
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = items.Count,
-
-                Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray()
-            };
-
-            return result;
-        }
-
-        public void Post(PostUpdatedSeries request)
-        {
-            var series = _libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Series).Name },
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-
-            }).Where(i => string.Equals(request.TvdbId, i.GetProviderId(MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            foreach (var item in series)
-            {
-                _libraryMonitor.ReportFileSystemChanged(item.Path);
-            }
-        }
-
-        public void Post(PostUpdatedMedia request)
-        {
-            if (request.Updates != null)
-            {
-                foreach (var item in request.Updates)
-                {
-                    _libraryMonitor.ReportFileSystemChanged(item.Path);
-                }
-            }
-        }
-
-        public void Post(PostUpdatedMovies request)
-        {
-            var movies = _libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Movie).Name },
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-
-            });
-
-            if (!string.IsNullOrWhiteSpace(request.ImdbId))
-            {
-                movies = movies.Where(i => string.Equals(request.ImdbId, i.GetProviderId(MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-            else if (!string.IsNullOrWhiteSpace(request.TmdbId))
-            {
-                movies = movies.Where(i => string.Equals(request.TmdbId, i.GetProviderId(MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-            else
-            {
-                movies = new List<BaseItem>();
-            }
-
-            foreach (var item in movies)
-            {
-                _libraryMonitor.ReportFileSystemChanged(item.Path);
-            }
-        }
-
-        public Task<object> Get(GetDownload request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            var user = auth.User;
-
-            if (user != null)
-            {
-                if (!item.CanDownload(user))
-                {
-                    throw new ArgumentException("Item does not support downloading");
-                }
-            }
-            else
-            {
-                if (!item.CanDownload())
-                {
-                    throw new ArgumentException("Item does not support downloading");
-                }
-            }
-
-            var headers = new Dictionary<string, string>();
-
-            if (user != null)
-            {
-                LogDownload(item, user, auth);
-            }
-
-            var path = item.Path;
-
-            // Quotes are valid in linux. They'll possibly cause issues here
-            var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
-            if (!string.IsNullOrWhiteSpace(filename))
-            {
-                // Kestrel doesn't support non-ASCII characters in headers
-                if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
-                {
-                    // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
-                    headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
-                }
-                else
-                {
-                    headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
-                }
-            }
-
-            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                Path = path,
-                ResponseHeaders = headers
-            });
-        }
-
-        private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
-        {
-            try
-            {
-                _activityManager.Create(new ActivityLog(
-                    string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
-                    "UserDownloadingContent",
-                    auth.UserId)
-                {
-                    ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
-                });
-            }
-            catch
-            {
-                // Logged at lower levels
-            }
-        }
-
-        public Task<object> Get(GetFile request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return ResultFactory.GetStaticFileResult(Request, item.Path);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPhyscialPaths request)
-        {
-            var result = _libraryManager.RootFolder.Children
-                .SelectMany(c => c.PhysicalLocations)
-                .ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetAncestors request)
-        {
-            var result = GetAncestors(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the ancestors.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto[]}.</returns>
-        public List<BaseItemDto> GetAncestors(GetAncestors request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var baseItemDtos = new List<BaseItemDto>();
-
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            BaseItem parent = item.GetParent();
-
-            while (parent != null)
-            {
-                if (user != null)
-                {
-                    parent = TranslateParentItem(parent, user);
-                }
-
-                baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
-
-                parent = parent.GetParent();
-            }
-
-            return baseItemDtos;
-        }
-
-        private BaseItem TranslateParentItem(BaseItem item, User user)
-        {
-            return item.GetParent() is AggregateFolder
-                ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
-                    .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
-                : item;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCriticReviews request)
-        {
-            return new QueryResult<BaseItemDto>();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItemCounts request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var counts = new ItemCounts
-            {
-                AlbumCount = GetCount(typeof(MusicAlbum), user, request),
-                EpisodeCount = GetCount(typeof(Episode), user, request),
-                MovieCount = GetCount(typeof(Movie), user, request),
-                SeriesCount = GetCount(typeof(Series), user, request),
-                SongCount = GetCount(typeof(Audio), user, request),
-                MusicVideoCount = GetCount(typeof(MusicVideo), user, request),
-                BoxSetCount = GetCount(typeof(BoxSet), user, request),
-                BookCount = GetCount(typeof(Book), user, request)
-            };
-
-            return ToOptimizedResult(counts);
-        }
-
-        private int GetCount(Type type, User user, GetItemCounts request)
-        {
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[] { type.Name },
-                Limit = 0,
-                Recursive = true,
-                IsVirtualItem = false,
-                IsFavorite = request.IsFavorite,
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-            };
-
-            return _libraryManager.GetItemsResult(query).TotalRecordCount;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(RefreshLibrary request)
-        {
-            try
-            {
-                await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error refreshing library");
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteItems request)
-        {
-            var ids = string.IsNullOrWhiteSpace(request.Ids)
-                ? Array.Empty<string>()
-                : request.Ids.Split(',');
-
-            foreach (var i in ids)
-            {
-                var item = _libraryManager.GetItemById(i);
-                var auth = _authContext.GetAuthorizationInfo(Request);
-                var user = auth.User;
-
-                if (!item.CanDelete(user))
-                {
-                    if (ids.Length > 1)
-                    {
-                        throw new SecurityException("Unauthorized access");
-                    }
-
-                    continue;
-                }
-
-                _libraryManager.DeleteItem(item, new DeleteOptions
-                {
-                    DeleteFileLocation = true
-                }, true);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteItem request)
-        {
-            Delete(new DeleteItems
-            {
-                Ids = request.Id
-            });
-        }
-
-        public object Get(GetThemeMedia request)
-        {
-            var themeSongs = GetThemeSongs(new GetThemeSongs
-            {
-                InheritFromParent = request.InheritFromParent,
-                Id = request.Id,
-                UserId = request.UserId
-
-            });
-
-            var themeVideos = GetThemeVideos(new GetThemeVideos
-            {
-                InheritFromParent = request.InheritFromParent,
-                Id = request.Id,
-                UserId = request.UserId
-
-            });
-
-            return ToOptimizedResult(new AllThemeMediaResult
-            {
-                ThemeSongsResult = themeSongs,
-                ThemeVideosResult = themeVideos,
-
-                SoundtrackSongsResult = new ThemeMediaResult()
-            });
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetThemeSongs request)
-        {
-            var result = GetThemeSongs(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        private ThemeMediaResult GetThemeSongs(GetThemeSongs request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id)
-                           ? (!request.UserId.Equals(Guid.Empty)
-                                  ? _libraryManager.GetUserRootFolder()
-                                  : _libraryManager.RootFolder)
-                           : _libraryManager.GetItemById(request.Id);
-
-            if (item == null)
-            {
-                throw new ResourceNotFoundException("Item not found.");
-            }
-
-            IEnumerable<BaseItem> themeItems;
-
-            while (true)
-            {
-                themeItems = item.GetThemeSongs();
-
-                if (themeItems.Any() || !request.InheritFromParent)
-                {
-                    break;
-                }
-
-                var parent = item.GetParent();
-                if (parent == null)
-                {
-                    break;
-                }
-                item = parent;
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var items = themeItems
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            return new ThemeMediaResult
-            {
-                Items = items,
-                TotalRecordCount = items.Length,
-                OwnerId = item.Id
-            };
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetThemeVideos request)
-        {
-            return ToOptimizedResult(GetThemeVideos(request));
-        }
-
-        public ThemeMediaResult GetThemeVideos(GetThemeVideos request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id)
-                           ? (!request.UserId.Equals(Guid.Empty)
-                                  ? _libraryManager.GetUserRootFolder()
-                                  : _libraryManager.RootFolder)
-                           : _libraryManager.GetItemById(request.Id);
-
-            if (item == null)
-            {
-                throw new ResourceNotFoundException("Item not found.");
-            }
-
-            IEnumerable<BaseItem> themeItems;
-
-            while (true)
-            {
-                themeItems = item.GetThemeVideos();
-
-                if (themeItems.Any() || !request.InheritFromParent)
-                {
-                    break;
-                }
-
-                var parent = item.GetParent();
-                if (parent == null)
-                {
-                    break;
-                }
-                item = parent;
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = themeItems
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            return new ThemeMediaResult
-            {
-                Items = items,
-                TotalRecordCount = items.Length,
-                OwnerId = item.Id
-            };
-        }
-    }
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index d703bdb058..cd329c94f9 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -14,6 +14,10 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="Library" />
+  </ItemGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

From 45234e5ecd787c4d2e9aadae8459917f3baee045 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 13:29:14 -0600
Subject: [PATCH 217/463] Add movie support to existing GetSimilarItemsResult

---
 Jellyfin.Api/Controllers/LibraryController.cs | 46 ++++++++++---------
 1 file changed, 24 insertions(+), 22 deletions(-)

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index e3e2e94894..92843a3737 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -20,6 +20,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Activity;
@@ -690,23 +691,7 @@ namespace Jellyfin.Api.Controllers
                 : _libraryManager.GetItemById(itemId);
 
             var program = item as IHasProgramAttributes;
-            if (item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer)
-            {
-                /*
-                 * // TODO
-                return new MoviesService(
-                    _moviesServiceLogger,
-                    ServerConfigurationManager,
-                    ResultFactory,
-                    _userManager,
-                    _libraryManager,
-                    _dtoService,
-                    _authContext)
-                {
-                    Request = Request,
-                }.GetSimilarItemsResult(request);*/
-            }
-
+            var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
             if (program != null && program.IsSeries)
             {
                 return GetSimilarItemsResult(
@@ -715,7 +700,8 @@ namespace Jellyfin.Api.Controllers
                     userId,
                     limit,
                     fields,
-                    new[] { nameof(Series) });
+                    new[] { nameof(Series) },
+                    false);
             }
 
             if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
@@ -729,7 +715,8 @@ namespace Jellyfin.Api.Controllers
                 userId,
                 limit,
                 fields,
-                new[] { item.GetType().Name });
+                new[] { item.GetType().Name },
+                isMovie);
         }
 
         /// <summary>
@@ -885,7 +872,8 @@ namespace Jellyfin.Api.Controllers
             Guid userId,
             int? limit,
             string fields,
-            string[] includeItemTypes)
+            string[] includeItemTypes,
+            bool isMovie)
         {
             var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
             var dtoOptions = new DtoOptions()
@@ -896,9 +884,11 @@ namespace Jellyfin.Api.Controllers
             {
                 Limit = limit,
                 IncludeItemTypes = includeItemTypes,
+                IsMovie = isMovie,
                 SimilarTo = item,
                 DtoOptions = dtoOptions,
-                EnableTotalRecordCount = false
+                EnableTotalRecordCount = !isMovie,
+                EnableGroupByMetadataKey = isMovie
             };
 
             // ExcludeArtistIds
@@ -909,7 +899,19 @@ namespace Jellyfin.Api.Controllers
 
             List<BaseItem> itemsResult;
 
-            if (item is MusicArtist)
+            if (isMovie)
+            {
+                var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+                if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+                {
+                    itemTypes.Add(nameof(Trailer));
+                    itemTypes.Add(nameof(LiveTvProgram));
+                }
+
+                query.IncludeItemTypes = itemTypes.ToArray();
+                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
+            }
+            else if (item is MusicArtist)
             {
                 query.IncludeItemTypes = Array.Empty<string>();
 

From 7c79ee0cd576ffe49f4268d70f4e63405fab9ea2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 13:51:01 -0600
Subject: [PATCH 218/463] Move MoviesService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/MoviesController.cs | 340 +++++++++++++++++++
 MediaBrowser.Api/Movies/MoviesService.cs     | 322 ------------------
 2 files changed, 340 insertions(+), 322 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/MoviesController.cs

diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
new file mode 100644
index 0000000000..ef2de07e86
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Movies controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class MoviesController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MoviesController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public MoviesController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Gets movie recommendations.
+        /// </summary>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="enableImages">(Unused) Optional. include image information in output.</param>
+        /// <param name="enableUserData">(Unused) Optional. include user data.</param>
+        /// <param name="imageTypeLimit">(Unused) Optional. the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">(Unused) Optional. The image types to include in the output.</param>
+        /// <param name="fields">Optional. The fields to return.</param>
+        /// <param name="categoryLimit">The max number of categories to return.</param>
+        /// <param name="itemLimit">The max number of items to return per category.</param>
+        /// <response code="200">Movie recommendations returned.</response>
+        /// <returns>The list of movie recommendations.</returns>
+        [HttpGet("Recommendations")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+        public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
+            [FromQuery] Guid userId,
+            [FromQuery] string parentId,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string fields,
+            [FromQuery] int categoryLimit = 5,
+            [FromQuery] int itemLimit = 8)
+        {
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request);
+
+            var categories = new List<RecommendationDto>();
+
+            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+
+            var query = new InternalItemsQuery(user)
+            {
+                IncludeItemTypes = new[]
+                {
+                    nameof(Movie),
+                    // typeof(Trailer).Name,
+                    // typeof(LiveTvProgram).Name
+                },
+                // IsMovie = true
+                OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+                Limit = 7,
+                ParentId = parentIdGuid,
+                Recursive = true,
+                IsPlayed = true,
+                DtoOptions = dtoOptions
+            };
+
+            var recentlyPlayedMovies = _libraryManager.GetItemList(query);
+
+            var itemTypes = new List<string> { nameof(Movie) };
+            if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+            {
+                itemTypes.Add(nameof(Trailer));
+                itemTypes.Add(nameof(LiveTvProgram));
+            }
+
+            var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+            {
+                IncludeItemTypes = itemTypes.ToArray(),
+                IsMovie = true,
+                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+                Limit = 10,
+                IsFavoriteOrLiked = true,
+                ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
+                EnableGroupByMetadataKey = true,
+                ParentId = parentIdGuid,
+                Recursive = true,
+                DtoOptions = dtoOptions
+            });
+
+            var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
+            // Get recently played directors
+            var recentDirectors = GetDirectors(mostRecentMovies)
+                .ToList();
+
+            // Get recently played actors
+            var recentActors = GetActors(mostRecentMovies)
+                .ToList();
+
+            var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
+            var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
+
+            var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
+            var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
+
+            var categoryTypes = new List<IEnumerator<RecommendationDto>>
+            {
+                // Give this extra weight
+                similarToRecentlyPlayed,
+                similarToRecentlyPlayed,
+
+                // Give this extra weight
+                similarToLiked,
+                similarToLiked,
+                hasDirectorFromRecentlyPlayed,
+                hasActorFromRecentlyPlayed
+            };
+
+            while (categories.Count < categoryLimit)
+            {
+                var allEmpty = true;
+
+                foreach (var category in categoryTypes)
+                {
+                    if (category.MoveNext())
+                    {
+                        categories.Add(category.Current);
+                        allEmpty = false;
+
+                        if (categories.Count >= categoryLimit)
+                        {
+                            break;
+                        }
+                    }
+                }
+
+                if (allEmpty)
+                {
+                    break;
+                }
+            }
+
+            return Ok(categories.OrderBy(i => i.RecommendationType));
+        }
+
+        private IEnumerable<RecommendationDto> GetWithDirector(
+            User user,
+            IEnumerable<string> names,
+            int itemLimit,
+            DtoOptions dtoOptions,
+            RecommendationType type)
+        {
+            var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+            if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+            {
+                itemTypes.Add(nameof(Trailer));
+                itemTypes.Add(nameof(LiveTvProgram));
+            }
+
+            foreach (var name in names)
+            {
+                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+                    {
+                        Person = name,
+                        // Account for duplicates by imdb id, since the database doesn't support this yet
+                        Limit = itemLimit + 2,
+                        PersonTypes = new[] { PersonType.Director },
+                        IncludeItemTypes = itemTypes.ToArray(),
+                        IsMovie = true,
+                        EnableGroupByMetadataKey = true,
+                        DtoOptions = dtoOptions
+                    }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+                    .Select(x => x.First())
+                    .Take(itemLimit)
+                    .ToList();
+
+                if (items.Count > 0)
+                {
+                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = name,
+                        CategoryId = name.GetMD5(),
+                        RecommendationType = type,
+                        Items = returnItems
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+        {
+            var itemTypes = new List<string> { nameof(Movie) };
+            if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+            {
+                itemTypes.Add(nameof(Trailer));
+                itemTypes.Add(nameof(LiveTvProgram));
+            }
+
+            foreach (var name in names)
+            {
+                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+                    {
+                        Person = name,
+                        // Account for duplicates by imdb id, since the database doesn't support this yet
+                        Limit = itemLimit + 2,
+                        IncludeItemTypes = itemTypes.ToArray(),
+                        IsMovie = true,
+                        EnableGroupByMetadataKey = true,
+                        DtoOptions = dtoOptions
+                    }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+                    .Select(x => x.First())
+                    .Take(itemLimit)
+                    .ToList();
+
+                if (items.Count > 0)
+                {
+                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = name,
+                        CategoryId = name.GetMD5(),
+                        RecommendationType = type,
+                        Items = returnItems
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<RecommendationDto> GetSimilarTo(User user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+        {
+            var itemTypes = new List<string> { nameof(Movie) };
+            if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+            {
+                itemTypes.Add(nameof(Trailer));
+                itemTypes.Add(nameof(LiveTvProgram));
+            }
+
+            foreach (var item in baselineItems)
+            {
+                var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
+                {
+                    Limit = itemLimit,
+                    IncludeItemTypes = itemTypes.ToArray(),
+                    IsMovie = true,
+                    SimilarTo = item,
+                    EnableGroupByMetadataKey = true,
+                    DtoOptions = dtoOptions
+                });
+
+                if (similar.Count > 0)
+                {
+                    var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
+
+                    yield return new RecommendationDto
+                    {
+                        BaselineItemName = item.Name,
+                        CategoryId = item.Id,
+                        RecommendationType = type,
+                        Items = returnItems
+                    };
+                }
+            }
+        }
+
+        private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+        {
+            var people = _libraryManager.GetPeople(new InternalPeopleQuery
+            {
+                ExcludePersonTypes = new[] { PersonType.Director },
+                MaxListOrder = 3
+            });
+
+            var itemIds = items.Select(i => i.Id).ToList();
+
+            return people
+                .Where(i => itemIds.Contains(i.ItemId))
+                .Select(i => i.Name)
+                .DistinctNames();
+        }
+
+        private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+        {
+            var people = _libraryManager.GetPeople(new InternalPeopleQuery
+            {
+                PersonTypes = new[] { PersonType.Director }
+            });
+
+            var itemIds = items.Select(i => i.Id).ToList();
+
+            return people
+                .Where(i => itemIds.Contains(i.ItemId))
+                .Select(i => i.Name)
+                .DistinctNames();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
index 2d61299c76..1931914d2b 100644
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ b/MediaBrowser.Api/Movies/MoviesService.cs
@@ -20,50 +20,6 @@ using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 
 namespace MediaBrowser.Api.Movies
 {
-    [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")]
-    public class GetMovieRecommendations : IReturn<RecommendationDto[]>, IHasDtoOptions
-    {
-        [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int CategoryLimit { get; set; }
-
-        [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int ItemLimit { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        public GetMovieRecommendations()
-        {
-            CategoryLimit = 5;
-            ItemLimit = 8;
-        }
-
-        public string Fields { get; set; }
-    }
-
     /// <summary>
     /// Class MoviesService
     /// </summary>
@@ -74,9 +30,7 @@ namespace MediaBrowser.Api.Movies
         /// The _user manager
         /// </summary>
         private readonly IUserManager _userManager;
-
         private readonly ILibraryManager _libraryManager;
-
         private readonly IDtoService _dtoService;
         private readonly IAuthorizationContext _authContext;
 
@@ -99,17 +53,6 @@ namespace MediaBrowser.Api.Movies
             _authContext = authContext;
         }
 
-        public object Get(GetMovieRecommendations request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = GetRecommendationCategories(user, request.ParentId, request.CategoryLimit, request.ItemLimit, dtoOptions);
-
-            return ToOptimizedResult(result);
-        }
-
         public QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request)
         {
             var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
@@ -149,270 +92,5 @@ namespace MediaBrowser.Api.Movies
 
             return result;
         }
-
-        private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions)
-        {
-            var categories = new List<RecommendationDto>();
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[]
-                {
-                    typeof(Movie).Name,
-                    //typeof(Trailer).Name,
-                    //typeof(LiveTvProgram).Name
-                },
-                // IsMovie = true
-                OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                Limit = 7,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                IsPlayed = true,
-                DtoOptions = dtoOptions
-            };
-
-            var recentlyPlayedMovies = _libraryManager.GetItemList(query);
-
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = itemTypes.ToArray(),
-                IsMovie = true,
-                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                Limit = 10,
-                IsFavoriteOrLiked = true,
-                ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
-                EnableGroupByMetadataKey = true,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = dtoOptions
-
-            });
-
-            var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
-            // Get recently played directors
-            var recentDirectors = GetDirectors(mostRecentMovies)
-                .ToList();
-
-            // Get recently played actors
-            var recentActors = GetActors(mostRecentMovies)
-                .ToList();
-
-            var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
-            var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
-
-            var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
-            var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
-
-            var categoryTypes = new List<IEnumerator<RecommendationDto>>
-            {
-                // Give this extra weight
-                similarToRecentlyPlayed,
-                similarToRecentlyPlayed,
-
-                // Give this extra weight
-                similarToLiked,
-                similarToLiked,
-
-                hasDirectorFromRecentlyPlayed,
-                hasActorFromRecentlyPlayed
-            };
-
-            while (categories.Count < categoryLimit)
-            {
-                var allEmpty = true;
-
-                foreach (var category in categoryTypes)
-                {
-                    if (category.MoveNext())
-                    {
-                        categories.Add(category.Current);
-                        allEmpty = false;
-
-                        if (categories.Count >= categoryLimit)
-                        {
-                            break;
-                        }
-                    }
-                }
-
-                if (allEmpty)
-                {
-                    break;
-                }
-            }
-
-            return categories.OrderBy(i => i.RecommendationType);
-        }
-
-        private IEnumerable<RecommendationDto> GetWithDirector(
-            User user,
-            IEnumerable<string> names,
-            int itemLimit,
-            DtoOptions dtoOptions,
-            RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var name in names)
-            {
-                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Person = name,
-                    // Account for duplicates by imdb id, since the database doesn't support this yet
-                    Limit = itemLimit + 2,
-                    PersonTypes = new[] { PersonType.Director },
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-
-                }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                .Select(x => x.First())
-                .Take(itemLimit)
-                .ToList();
-
-                if (items.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = name,
-                        CategoryId = name.GetMD5(),
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var name in names)
-            {
-                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Person = name,
-                    // Account for duplicates by imdb id, since the database doesn't support this yet
-                    Limit = itemLimit + 2,
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-
-                }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                .Select(x => x.First())
-                .Take(itemLimit)
-                .ToList();
-
-                if (items.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = name,
-                        CategoryId = name.GetMD5(),
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var item in baselineItems)
-            {
-                var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Limit = itemLimit,
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    SimilarTo = item,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-
-                });
-
-                if (similar.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = item.Name,
-                        CategoryId = item.Id,
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<string> GetActors(List<BaseItem> items)
-        {
-            var people = _libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                ExcludePersonTypes = new[]
-                {
-                    PersonType.Director
-                },
-                MaxListOrder = 3
-            });
-
-            var itemIds = items.Select(i => i.Id).ToList();
-
-            return people
-                .Where(i => itemIds.Contains(i.ItemId))
-                .Select(i => i.Name)
-                .DistinctNames();
-        }
-
-        private IEnumerable<string> GetDirectors(List<BaseItem> items)
-        {
-            var people = _libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                PersonTypes = new[]
-                {
-                    PersonType.Director
-                }
-            });
-
-            var itemIds = items.Select(i => i.Id).ToList();
-
-            return people
-                .Where(i => itemIds.Contains(i.ItemId))
-                .Select(i => i.Name)
-                .DistinctNames();
-        }
     }
 }

From f17d198eb5643f551973b1e54405a472fe0b55b2 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 19 Jun 2020 22:18:46 +0200
Subject: [PATCH 219/463] Move SuggestionsService to Jellyfin.Api

---
 .../Controllers/SuggestionsController.cs      | 86 ++++++++++++++++
 MediaBrowser.Api/SuggestionsService.cs        | 98 -------------------
 2 files changed, 86 insertions(+), 98 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SuggestionsController.cs
 delete mode 100644 MediaBrowser.Api/SuggestionsService.cs

diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
new file mode 100644
index 0000000000..2d6445c305
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The suggestions controller.
+    /// </summary>
+    public class SuggestionsController : BaseJellyfinApiController
+    {
+        private readonly IDtoService _dtoService;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SuggestionsController"/> class.
+        /// </summary>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public SuggestionsController(
+            IDtoService dtoService,
+            IUserManager userManager,
+            ILibraryManager libraryManager)
+        {
+            _dtoService = dtoService;
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets suggestions.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="mediaType">The media types.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
+        /// <param name="startIndex">Optional. The start index.</param>
+        /// <param name="limit">Optional. The limit.</param>
+        /// <response code="200">Suggestions returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
+        [HttpGet("/Users/{userId}/Suggestions")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
+            [FromRoute] Guid userId,
+            [FromQuery] string? mediaType,
+            [FromQuery] string? type,
+            [FromQuery] bool enableTotalRecordCount,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit)
+        {
+            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+            {
+                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+                MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                IsVirtualItem = false,
+                StartIndex = startIndex,
+                Limit = limit,
+                DtoOptions = dtoOptions,
+                EnableTotalRecordCount = enableTotalRecordCount,
+                Recursive = true
+            });
+
+            var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = result.TotalRecordCount,
+                Items = dtoList
+            };
+        }
+    }
+}
diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs
deleted file mode 100644
index 32d3bde5cb..0000000000
--- a/MediaBrowser.Api/SuggestionsService.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Users/{UserId}/Suggestions", "GET", Summary = "Gets items based on a query.")]
-    public class GetSuggestedItems : IReturn<QueryResult<BaseItemDto>>
-    {
-        public string MediaType { get; set; }
-        public string Type { get; set; }
-        public Guid UserId { get; set; }
-        public bool EnableTotalRecordCount { get; set; }
-        public int? StartIndex { get; set; }
-        public int? Limit { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (Type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-    }
-
-    public class SuggestionsService : BaseApiService
-    {
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-
-        public SuggestionsService(
-            ILogger<SuggestionsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            IUserManager userManager,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetSuggestedItems request)
-        {
-            return GetResultItems(request);
-        }
-
-        private QueryResult<BaseItemDto> GetResultItems(GetSuggestedItems request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var result = GetItems(request, user, dtoOptions);
-
-            var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = result.TotalRecordCount,
-                Items = dtoList
-            };
-        }
-
-        private QueryResult<BaseItem> GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions)
-        {
-            return _libraryManager.GetItemsResult(new InternalItemsQuery(user)
-            {
-                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                IsVirtualItem = false,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                DtoOptions = dtoOptions,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                Recursive = true
-            });
-        }
-    }
-}

From cd273c4e98246420edf39ef63c906fbc3725d8e4 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 19 Jun 2020 15:08:35 -0600
Subject: [PATCH 220/463] Start move ImageService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/ImageController.cs | 139 ++++++++++++++++++++
 MediaBrowser.Api/Images/ImageService.cs     |  77 -----------
 2 files changed, 139 insertions(+), 77 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ImageController.cs

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
new file mode 100644
index 0000000000..6742bffc61
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -0,0 +1,139 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Image controller.
+    /// </summary>
+    public class ImageController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IProviderManager _providerManager;
+        private readonly IImageProcessor _imageProcessor;
+        private readonly IFileSystem _fileSystem;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ILogger<ImageController> _logger;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImageController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public ImageController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IProviderManager providerManager,
+            IImageProcessor imageProcessor,
+            IFileSystem fileSystem,
+            IAuthorizationContext authContext,
+            ILogger<ImageController> logger,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _providerManager = providerManager;
+            _imageProcessor = imageProcessor;
+            _fileSystem = fileSystem;
+            _authContext = authContext;
+            _logger = logger;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Sets the user image.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="imageType">(Unused) Image type.</param>
+        /// <param name="index">(Unused) Image index.</param>
+        /// <response code="204">Image updated.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Users/{userId}/Images/{imageType}")]
+        [HttpPost("/Users/{userId}/Images/{imageType}/{index}")]
+        public async Task<ActionResult> PostUserImage(
+            [FromRoute] Guid userId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? index)
+        {
+            // TODO AssertCanUpdateUser(_authContext, _userManager, id, true);
+
+            var user = _userManager.GetUserById(userId);
+            await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
+            // Handle image/png; charset=utf-8
+            var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+            var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+
+            await _providerManager
+                .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
+                .ConfigureAwait(false);
+            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Delete the user's image.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="imageType">(Unused) Image type.</param>
+        /// <param name="index">(Unused) Image index.</param>
+        /// <response code="204">Image deleted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Users/{userId}/Images/{itemType}")]
+        [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")]
+        public ActionResult DeleteUserImage(
+            [FromRoute] Guid userId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? index)
+        {
+            // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true);
+
+            var user = _userManager.GetUserById(userId);
+            try
+            {
+                System.IO.File.Delete(user.ProfileImage.Path);
+            }
+            catch (IOException e)
+            {
+                _logger.LogError(e, "Error deleting user profile image:");
+            }
+
+            _userManager.ClearProfileImage(user);
+            return NoContent();
+        }
+
+        private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+        {
+            using var reader = new StreamReader(inputStream);
+            var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+            var bytes = Convert.FromBase64String(text);
+            return new MemoryStream(bytes)
+            {
+                Position = 0
+            };
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index 0b8ddeacdf..48c879bb72 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -163,44 +163,6 @@ namespace MediaBrowser.Api.Images
         public string Id { get; set; }
     }
 
-    /// <summary>
-    /// Class DeleteUserImage
-    /// </summary>
-    [Route("/Users/{Id}/Images/{Type}", "DELETE")]
-    [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")]
-    [Authenticated]
-    public class DeleteUserImage : DeleteImageRequest, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class PostUserImage
-    /// </summary>
-    [Route("/Users/{Id}/Images/{Type}", "POST")]
-    [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")]
-    [Authenticated]
-    public class PostUserImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The raw Http Request Input Stream
-        /// </summary>
-        /// <value>The request stream.</value>
-        public Stream RequestStream { get; set; }
-    }
-
     /// <summary>
     /// Class PostItemImage
     /// </summary>
@@ -438,23 +400,6 @@ namespace MediaBrowser.Api.Images
             return GetImage(request, item.Id, item, true);
         }
 
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(PostUserImage request)
-        {
-            var id = Guid.Parse(GetPathValue(1));
-
-            AssertCanUpdateUser(_authContext, _userManager, id, true);
-
-            request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
-
-            var user = _userManager.GetUserById(id);
-
-            return PostImage(user, request.RequestStream, Request.ContentType);
-        }
-
         /// <summary>
         /// Posts the specified request.
         /// </summary>
@@ -470,28 +415,6 @@ namespace MediaBrowser.Api.Images
             return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
         }
 
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteUserImage request)
-        {
-            var userId = request.Id;
-            AssertCanUpdateUser(_authContext, _userManager, userId, true);
-
-            var user = _userManager.GetUserById(userId);
-            try
-            {
-                File.Delete(user.ProfileImage.Path);
-            }
-            catch (IOException e)
-            {
-                Logger.LogError(e, "Error deleting user profile image:");
-            }
-
-            _userManager.ClearProfileImage(user);
-        }
-
         /// <summary>
         /// Deletes the specified request.
         /// </summary>

From 33de0ac10880a941a10f28c64f18253cc711b8da Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 20 Jun 2020 12:10:45 +0200
Subject: [PATCH 221/463] Use RequestHelpers.Split

---
 Jellyfin.Api/Controllers/SuggestionsController.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 2d6445c305..e1a99a1385 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -64,8 +65,8 @@ namespace Jellyfin.Api.Controllers
             var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
             {
                 OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                MediaTypes = (mediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
-                IncludeItemTypes = (type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
+                IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
                 IsVirtualItem = false,
                 StartIndex = startIndex,
                 Limit = limit,

From 64fb173dad77a38273548434bee683b85e323345 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 20 Jun 2020 15:59:41 +0200
Subject: [PATCH 222/463] Move DashboardController to Jellyfin.Api

---
 .../ApplicationHost.cs                        |   4 -
 .../Emby.Server.Implementations.csproj        |   1 -
 .../Controllers/DashboardController.cs        | 264 ++++++++++++++
 .../Models}/ConfigurationPageInfo.cs          |  38 +-
 Jellyfin.Server/Program.cs                    |   4 +-
 .../Api/DashboardService.cs                   | 340 ------------------
 .../MediaBrowser.WebDashboard.csproj          |  42 ---
 .../Properties/AssemblyInfo.cs                |  21 --
 MediaBrowser.WebDashboard/ServerEntryPoint.cs |  42 ---
 MediaBrowser.sln                              |   6 -
 10 files changed, 296 insertions(+), 466 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/DashboardController.cs
 rename {MediaBrowser.WebDashboard/Api => Jellyfin.Api/Models}/ConfigurationPageInfo.cs (55%)
 delete mode 100644 MediaBrowser.WebDashboard/Api/DashboardService.cs
 delete mode 100644 MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
 delete mode 100644 MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
 delete mode 100644 MediaBrowser.WebDashboard/ServerEntryPoint.cs

diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5772dd479d..25ee7e9ec0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
@@ -1037,9 +1036,6 @@ namespace Emby.Server.Implementations
             // Include composable parts in the Api assembly
             yield return typeof(ApiEntryPoint).Assembly;
 
-            // Include composable parts in the Dashboard assembly
-            yield return typeof(DashboardService).Assembly;
-
             // Include composable parts in the Model assembly
             yield return typeof(SystemInfo).Assembly;
 
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index e71e437acd..5272e2692f 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -13,7 +13,6 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
-    <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
     <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
new file mode 100644
index 0000000000..6a7bf7d0aa
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using Jellyfin.Api.Models;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The dashboard controller.
+    /// </summary>
+    public class DashboardController : BaseJellyfinApiController
+    {
+        private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _appConfig;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IResourceFileManager _resourceFileManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DashboardController"/> class.
+        /// </summary>
+        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
+        /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+        public DashboardController(
+            IServerApplicationHost appHost,
+            IConfiguration appConfig,
+            IResourceFileManager resourceFileManager,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _appHost = appHost;
+            _appConfig = appConfig;
+            _resourceFileManager = resourceFileManager;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Gets the path of the directory containing the static web interface content, or null if the server is not
+        /// hosting the web client.
+        /// </summary>
+        private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
+
+        /// <summary>
+        /// Gets the configuration pages.
+        /// </summary>
+        /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
+        /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param>
+        /// <response code="200">ConfigurationPages returned.</response>
+        /// <response code="404">Server still loading.</response>
+        /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
+        [HttpGet("/web/ConfigurationPages")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
+            [FromQuery] bool? enableInMainMenu,
+            [FromQuery] ConfigurationPageType? pageType)
+        {
+            const string unavailableMessage = "The server is still loading. Please try again momentarily.";
+
+            var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
+
+            if (pages == null)
+            {
+                return NotFound(unavailableMessage);
+            }
+
+            // Don't allow a failing plugin to fail them all
+            var configPages = pages.Select(p =>
+                {
+                    return new ConfigurationPageInfo(p);
+                })
+                .Where(i => i != null)
+                .ToList();
+
+            configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
+
+            if (pageType != null)
+            {
+                configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList();
+            }
+
+            if (enableInMainMenu.HasValue)
+            {
+                configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
+            }
+
+            return configPages;
+        }
+
+        /// <summary>
+        /// Gets a dashboard configuration page.
+        /// </summary>
+        /// <param name="name">The name of the page.</param>
+        /// <response code="200">ConfigurationPage returned.</response>
+        /// <response code="404">Plugin configuration page not found.</response>
+        /// <returns>The configuration page.</returns>
+        [HttpGet("/web/ConfigurationPage")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult GetDashboardConfigurationPage([FromQuery] string name)
+        {
+            IPlugin? plugin = null;
+            Stream? stream = null;
+
+            var isJs = false;
+            var isTemplate = false;
+
+            var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
+            if (page != null)
+            {
+                plugin = page.Plugin;
+                stream = page.GetHtmlStream();
+            }
+
+            if (plugin == null)
+            {
+                var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
+                if (altPage != null)
+                {
+                    plugin = altPage.Item2;
+                    stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
+
+                    isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
+                    isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
+                }
+            }
+
+            if (plugin != null && stream != null)
+            {
+                if (isJs)
+                {
+                    return File(stream, MimeTypes.GetMimeType("page.js"));
+                }
+
+                if (isTemplate)
+                {
+                    return File(stream, MimeTypes.GetMimeType("page.html"));
+                }
+
+                return File(stream, MimeTypes.GetMimeType("page.html"));
+            }
+
+            return NotFound();
+        }
+
+        /// <summary>
+        /// Gets the robots.txt.
+        /// </summary>
+        /// <response code="200">Robots.txt returned.</response>
+        /// <returns>The robots.txt.</returns>
+        [HttpGet("/robots.txt")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ApiExplorerSettings(IgnoreApi = true)]
+        public ActionResult GetRobotsTxt()
+        {
+            return GetWebClientResource("robots.txt", string.Empty);
+        }
+
+        /// <summary>
+        /// Gets a resource from the web client.
+        /// </summary>
+        /// <param name="resourceName">The resource name.</param>
+        /// <param name="v">The v.</param>
+        /// <response code="200">Web client returned.</response>
+        /// <response code="404">Server does not host a web client.</response>
+        /// <returns>The resource.</returns>
+        [HttpGet("/web/{*resourceName}")]
+        [ApiExplorerSettings(IgnoreApi = true)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")]
+        public ActionResult GetWebClientResource(
+            [FromRoute] string resourceName,
+            [FromQuery] string? v)
+        {
+            if (!_appConfig.HostWebClient() || WebClientUiPath == null)
+            {
+                return NotFound("Server does not host a web client.");
+            }
+
+            var path = resourceName;
+            var basePath = WebClientUiPath;
+
+            // Bounce them to the startup wizard if it hasn't been completed yet
+            if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
+                && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase)
+                && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase))
+            {
+                return Redirect("index.html?start=wizard#!/wizardstart.html");
+            }
+
+            var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
+            return File(stream, MimeTypes.GetMimeType(path));
+        }
+
+        /// <summary>
+        /// Gets the favicon.
+        /// </summary>
+        /// <response code="200">Favicon.ico returned.</response>
+        /// <returns>The favicon.</returns>
+        [HttpGet("/favicon.ico")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ApiExplorerSettings(IgnoreApi = true)]
+        public ActionResult GetFavIcon()
+        {
+            return GetWebClientResource("favicon.ico", string.Empty);
+        }
+
+        /// <summary>
+        /// Gets the path of the directory containing the static web interface content.
+        /// </summary>
+        /// <param name="appConfig">The app configuration.</param>
+        /// <param name="serverConfigManager">The server configuration manager.</param>
+        /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
+        public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
+        {
+            if (!appConfig.HostWebClient())
+            {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
+            {
+                return serverConfigManager.Configuration.DashboardSourcePath;
+            }
+
+            return serverConfigManager.ApplicationPaths.WebPath;
+        }
+
+        private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
+        {
+            return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
+        }
+
+        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
+        {
+            var hasConfig = plugin as IHasWebPages;
+
+            if (hasConfig == null)
+            {
+                return new List<Tuple<PluginPageInfo, IPlugin>>();
+            }
+
+            return hasConfig.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
+        }
+
+        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
+        {
+            return _appHost.Plugins.SelectMany(GetPluginPages);
+        }
+    }
+}
diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
similarity index 55%
rename from MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs
rename to Jellyfin.Api/Models/ConfigurationPageInfo.cs
index e49a4be8af..2aa6373aa9 100644
--- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs
+++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
@@ -1,13 +1,18 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Plugins;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Plugins;
 
-namespace MediaBrowser.WebDashboard.Api
+namespace Jellyfin.Api.Models
 {
+    /// <summary>
+    /// The configuration page info.
+    /// </summary>
     public class ConfigurationPageInfo
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+        /// </summary>
+        /// <param name="page">Instance of <see cref="IPluginConfigurationPage"/> interface.</param>
         public ConfigurationPageInfo(IPluginConfigurationPage page)
         {
             Name = page.Name;
@@ -22,6 +27,11 @@ namespace MediaBrowser.WebDashboard.Api
             }
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+        /// </summary>
+        /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
+        /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
         public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page)
         {
             Name = page.Name;
@@ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api
         /// <value>The name.</value>
         public string Name { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the configurations page is enabled in the main menu.
+        /// </summary>
         public bool EnableInMainMenu { get; set; }
 
-        public string MenuSection { get; set; }
+        /// <summary>
+        /// Gets or sets the menu section.
+        /// </summary>
+        public string? MenuSection { get; set; }
 
-        public string MenuIcon { get; set; }
+        /// <summary>
+        /// Gets or sets the menu icon.
+        /// </summary>
+        public string? MenuIcon { get; set; }
 
-        public string DisplayName { get; set; }
+        /// <summary>
+        /// Gets or sets the display name.
+        /// </summary>
+        public string? DisplayName { get; set; }
 
         /// <summary>
         /// Gets or sets the type of the configuration page.
@@ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api
         /// Gets or sets the plugin id.
         /// </summary>
         /// <value>The plugin id.</value>
-        public string PluginId { get; set; }
+        public string? PluginId { get; set; }
     }
 }
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3971a08e91..dfc7bbbb10 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -14,9 +14,9 @@ using Emby.Server.Implementations;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Networking;
+using Jellyfin.Api.Controllers;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Extensions;
-using MediaBrowser.WebDashboard.Api;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.Extensions.Configuration;
@@ -172,7 +172,7 @@ namespace Jellyfin.Server
                 // If hosting the web client, validate the client content path
                 if (startupConfig.HostWebClient())
                 {
-                    string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager);
+                    string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager);
                     if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
                     {
                         throw new InvalidOperationException(
diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs
deleted file mode 100644
index 63cbfd9e42..0000000000
--- a/MediaBrowser.WebDashboard/Api/DashboardService.cs
+++ /dev/null
@@ -1,340 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.WebDashboard.Api
-{
-    /// <summary>
-    /// Class GetDashboardConfigurationPages.
-    /// </summary>
-    [Route("/web/ConfigurationPages", "GET")]
-    public class GetDashboardConfigurationPages : IReturn<List<ConfigurationPageInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the type of the page.
-        /// </summary>
-        /// <value>The type of the page.</value>
-        public ConfigurationPageType? PageType { get; set; }
-
-        public bool? EnableInMainMenu { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetDashboardConfigurationPage.
-    /// </summary>
-    [Route("/web/ConfigurationPage", "GET")]
-    public class GetDashboardConfigurationPage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-    }
-
-    [Route("/robots.txt", "GET", IsHidden = true)]
-    public class GetRobotsTxt
-    {
-    }
-
-    /// <summary>
-    /// Class GetDashboardResource.
-    /// </summary>
-    [Route("/web/{ResourceName*}", "GET", IsHidden = true)]
-    public class GetDashboardResource
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string ResourceName { get; set; }
-
-        /// <summary>
-        /// Gets or sets the V.
-        /// </summary>
-        /// <value>The V.</value>
-        public string V { get; set; }
-    }
-
-    [Route("/favicon.ico", "GET", IsHidden = true)]
-    public class GetFavIcon
-    {
-    }
-
-    /// <summary>
-    /// Class DashboardService.
-    /// </summary>
-    public class DashboardService : IService, IRequiresRequest
-    {
-        /// <summary>
-        /// Gets or sets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        private readonly ILogger<DashboardService> _logger;
-
-        /// <summary>
-        /// Gets or sets the HTTP result factory.
-        /// </summary>
-        /// <value>The HTTP result factory.</value>
-        private readonly IHttpResultFactory _resultFactory;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IConfiguration _appConfig;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IResourceFileManager _resourceFileManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DashboardService" /> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="appHost">The application host.</param>
-        /// <param name="appConfig">The application configuration.</param>
-        /// <param name="resourceFileManager">The resource file manager.</param>
-        /// <param name="serverConfigurationManager">The server configuration manager.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="resultFactory">The result factory.</param>
-        public DashboardService(
-            ILogger<DashboardService> logger,
-            IServerApplicationHost appHost,
-            IConfiguration appConfig,
-            IResourceFileManager resourceFileManager,
-            IServerConfigurationManager serverConfigurationManager,
-            IFileSystem fileSystem,
-            IHttpResultFactory resultFactory)
-        {
-            _logger = logger;
-            _appHost = appHost;
-            _appConfig = appConfig;
-            _resourceFileManager = resourceFileManager;
-            _serverConfigurationManager = serverConfigurationManager;
-            _fileSystem = fileSystem;
-            _resultFactory = resultFactory;
-        }
-
-        /// <summary>
-        /// Gets or sets the request context.
-        /// </summary>
-        /// <value>The request context.</value>
-        public IRequest Request { get; set; }
-
-        /// <summary>
-        /// Gets the path of the directory containing the static web interface content, or null if the server is not
-        /// hosting the web client.
-        /// </summary>
-        public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager);
-
-        /// <summary>
-        /// Gets the path of the directory containing the static web interface content.
-        /// </summary>
-        /// <param name="appConfig">The app configuration.</param>
-        /// <param name="serverConfigManager">The server configuration manager.</param>
-        /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
-        public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
-        {
-            if (!appConfig.HostWebClient())
-            {
-                return null;
-            }
-
-            if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
-            {
-                return serverConfigManager.Configuration.DashboardSourcePath;
-            }
-
-            return serverConfigManager.ApplicationPaths.WebPath;
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetFavIcon request)
-        {
-            return Get(new GetDashboardResource
-            {
-                ResourceName = "favicon.ico"
-            });
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public Task<object> Get(GetDashboardConfigurationPage request)
-        {
-            IPlugin plugin = null;
-            Stream stream = null;
-
-            var isJs = false;
-            var isTemplate = false;
-
-            var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase));
-            if (page != null)
-            {
-                plugin = page.Plugin;
-                stream = page.GetHtmlStream();
-            }
-
-            if (plugin == null)
-            {
-                var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, request.Name, StringComparison.OrdinalIgnoreCase));
-                if (altPage != null)
-                {
-                    plugin = altPage.Item2;
-                    stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
-
-                    isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
-                    isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
-                }
-            }
-
-            if (plugin != null && stream != null)
-            {
-                if (isJs)
-                {
-                    return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream));
-                }
-
-                if (isTemplate)
-                {
-                    return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
-                }
-
-                return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
-            }
-
-            throw new ResourceNotFoundException();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetDashboardConfigurationPages request)
-        {
-            const string unavailableMessage = "The server is still loading. Please try again momentarily.";
-
-            var instance = ServerEntryPoint.Instance;
-
-            if (instance == null)
-            {
-                throw new InvalidOperationException(unavailableMessage);
-            }
-
-            var pages = instance.PluginConfigurationPages;
-
-            if (pages == null)
-            {
-                throw new InvalidOperationException(unavailableMessage);
-            }
-
-            // Don't allow a failing plugin to fail them all
-            var configPages = pages.Select(p =>
-            {
-                try
-                {
-                    return new ConfigurationPageInfo(p);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
-                    return null;
-                }
-            })
-                .Where(i => i != null)
-                .ToList();
-
-            configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
-
-            if (request.PageType.HasValue)
-            {
-                configPages = configPages.Where(p => p.ConfigurationPageType == request.PageType.Value).ToList();
-            }
-
-            if (request.EnableInMainMenu.HasValue)
-            {
-                configPages = configPages.Where(p => p.EnableInMainMenu == request.EnableInMainMenu.Value).ToList();
-            }
-
-            return configPages;
-        }
-
-        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
-        {
-            return _appHost.Plugins.SelectMany(GetPluginPages);
-        }
-
-        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
-        {
-            var hasConfig = plugin as IHasWebPages;
-
-            if (hasConfig == null)
-            {
-                return new List<Tuple<PluginPageInfo, IPlugin>>();
-            }
-
-            return hasConfig.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
-        }
-
-        private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
-        {
-            return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
-        }
-
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
-        public object Get(GetRobotsTxt request)
-        {
-            return Get(new GetDashboardResource
-            {
-                ResourceName = "robots.txt"
-            });
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetDashboardResource request)
-        {
-            if (!_appConfig.HostWebClient() || DashboardUIPath == null)
-            {
-                throw new ResourceNotFoundException();
-            }
-
-            var path = request?.ResourceName;
-            var basePath = DashboardUIPath;
-
-            // Bounce them to the startup wizard if it hasn't been completed yet
-            if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
-                && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase)
-                && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase))
-            {
-                Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html");
-                return null;
-            }
-
-            return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false);
-        }
-    }
-}
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
deleted file mode 100644
index bcaee50f29..0000000000
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ /dev/null
@@ -1,42 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
-  <PropertyGroup>
-    <ProjectGuid>{5624B7B5-B5A7-41D8-9F10-CC5611109619}</ProjectGuid>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Compile Include="..\SharedVersion.cs" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Include="jellyfin-web\**\*.*">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
-
-  <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
-    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
-    <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-  </PropertyGroup>
-
-  <!-- Code Analyzers-->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
-    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
-  </ItemGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
-</Project>
diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
deleted file mode 100644
index 584d490216..0000000000
--- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.WebDashboard")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright ©  2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components.  If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs
deleted file mode 100644
index 5c7e8b3c76..0000000000
--- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Controller.Plugins;
-
-namespace MediaBrowser.WebDashboard
-{
-    public sealed class ServerEntryPoint : IServerEntryPoint
-    {
-        private readonly IApplicationHost _appHost;
-
-        public ServerEntryPoint(IApplicationHost appHost)
-        {
-            _appHost = appHost;
-            Instance = this;
-        }
-
-        public static ServerEntryPoint Instance { get; private set; }
-
-        /// <summary>
-        /// Gets the list of plugin configuration pages.
-        /// </summary>
-        /// <value>The configuration pages.</value>
-        public List<IPluginConfigurationPage> PluginConfigurationPages { get; private set; }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            PluginConfigurationPages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
-
-            return Task.CompletedTask;
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-        }
-    }
-}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index e100c0b1cd..0362eff1c8 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "Medi
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}"
@@ -94,10 +92,6 @@ Global
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU
-		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
 		{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU

From 0c98bc42a8726dfa09244d55acf5339b3bd7a403 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 15:11:10 -0600
Subject: [PATCH 223/463] Fix response code & docs

---
 .../Controllers/ScheduledTasksController.cs        | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index f7122c4134..64de23ef2a 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -88,9 +88,9 @@ namespace Jellyfin.Api.Controllers
         /// Start specified task.
         /// </summary>
         /// <param name="taskId">Task Id.</param>
-        /// <response code="200">Task started.</response>
+        /// <response code="204">Task started.</response>
         /// <response code="404">Task not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -105,14 +105,14 @@ namespace Jellyfin.Api.Controllers
             }
 
             _taskManager.Execute(task, new TaskOptions());
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
         /// Stop specified task.
         /// </summary>
         /// <param name="taskId">Task Id.</param>
-        /// <response code="200">Task stopped.</response>
+        /// <response code="204">Task stopped.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpDelete("Running/{taskId}")]
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             _taskManager.Cancel(task);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -137,7 +137,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="taskId">Task Id.</param>
         /// <param name="triggerInfos">Triggers.</param>
-        /// <response code="200">Task triggers updated.</response>
+        /// <response code="204">Task triggers updated.</response>
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("{taskId}/Triggers")]
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             task.Triggers = triggerInfos;
-            return Ok();
+            return NoContent();
         }
     }
 }

From 95bae56640289cf11886b0036fb6a685353e3dc4 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 15:14:43 -0600
Subject: [PATCH 224/463] Fix response code & docs

---
 Jellyfin.Api/Controllers/ItemUpdateController.cs | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 2537996512..0c66ff875f 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -60,9 +60,9 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="itemId">The item id.</param>
         /// <param name="request">The new item properties.</param>
-        /// <response code="200">Item updated.</response>
+        /// <response code="204">Item updated.</response>
         /// <response code="404">Item not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
         [HttpPost("/Items/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
                     RefreshPriority.High);
             }
 
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -187,9 +187,9 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="itemId">The item id.</param>
         /// <param name="contentType">The content type of the item.</param>
-        /// <response code="200">Item content type updated.</response>
+        /// <response code="204">Item content type updated.</response>
         /// <response code="404">Item not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
         [HttpPost("/Items/{itemId}/ContentType")]
         public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType)
         {
@@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers
 
             _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
             _serverConfigurationManager.SaveConfiguration();
-            return Ok();
+            return NoContent();
         }
 
         private void UpdateItem(BaseItemDto request, BaseItem item)

From 8c38644fcadda10dd6132c3f43b333ccdeb7b392 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 15:15:59 -0600
Subject: [PATCH 225/463] Fix response code & docs

---
 Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 0c66ff875f..384f250ecc 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
         [HttpPost("/Items/{itemId}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request)
         {
@@ -191,6 +191,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
         [HttpPost("/Items/{itemId}/ContentType")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType)
         {
             var item = _libraryManager.GetItemById(itemId);

From 81c0451b5e578bb8a41dcb81f2766dbd1eb7f055 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 15:16:30 -0600
Subject: [PATCH 226/463] Fix response code & docs

---
 Jellyfin.Api/Controllers/ScheduledTasksController.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 64de23ef2a..bf5c3076e0 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("Running/{taskId}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult StartTask([FromRoute] string taskId)
         {
@@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpDelete("Running/{taskId}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult StopTask([FromRoute] string taskId)
         {
@@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Task not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpPost("{taskId}/Triggers")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateTask(
             [FromRoute] string taskId,

From d1ca0cb4c7161b420c32e48824cc5065054b1869 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 16:03:19 -0600
Subject: [PATCH 227/463] Use proper DtoOptions extensions

---
 .../Controllers/PlaylistsController.cs        | 33 ++++++++++---------
 Jellyfin.Api/Extensions/DtoExtensions.cs      |  2 +-
 Jellyfin.Api/Helpers/RequestHelpers.cs        | 18 ++++++++++
 3 files changed, 37 insertions(+), 16 deletions(-)

diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 0d73962de4..9e2a91e102 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -4,6 +4,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaylistDtos;
 using MediaBrowser.Controller.Dto;
@@ -80,17 +81,17 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playlistId">The playlist id.</param>
         /// <param name="ids">Item id, comma delimited.</param>
         /// <param name="userId">The userId.</param>
-        /// <response code="200">Items added to playlist.</response>
-        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        /// <response code="204">Items added to playlist.</response>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
         [HttpPost("{playlistId}/Items")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddToPlaylist(
             [FromRoute] string playlistId,
             [FromQuery] string ids,
             [FromQuery] Guid userId)
         {
             _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -99,17 +100,17 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playlistId">The playlist id.</param>
         /// <param name="itemId">The item id.</param>
         /// <param name="newIndex">The new index.</param>
-        /// <response code="200">Item moved to new index.</response>
-        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        /// <response code="204">Item moved to new index.</response>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
         [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult MoveItem(
             [FromRoute] string playlistId,
             [FromRoute] string itemId,
             [FromRoute] int newIndex)
         {
             _playlistManager.MoveItem(playlistId, itemId, newIndex);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -117,14 +118,14 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="playlistId">The playlist id.</param>
         /// <param name="entryIds">The item ids, comma delimited.</param>
-        /// <response code="200">Items removed.</response>
-        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        /// <response code="204">Items removed.</response>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
         [HttpDelete("{playlistId}/Items")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds)
         {
             _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(','));
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -151,7 +152,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] string fields,
             [FromRoute] bool? enableImages,
             [FromRoute] bool? enableUserData,
-            [FromRoute] bool? imageTypeLimit,
+            [FromRoute] int? imageTypeLimit,
             [FromRoute] string enableImageTypes)
         {
             var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
@@ -176,8 +177,10 @@ namespace Jellyfin.Api.Controllers
                 items = items.Take(limit.Value).ToArray();
             }
 
-            // TODO var dtoOptions = GetDtoOptions(_authContext, request);
-            var dtoOptions = new DtoOptions();
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
             var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
 
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 4c587391fc..ac248109d7 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Extensions
         /// <param name="enableImageTypes">Enable image types.</param>
         /// <returns>Modified DtoOptions object.</returns>
         internal static DtoOptions AddAdditionalDtoOptions(
-            in DtoOptions dtoOptions,
+            this DtoOptions dtoOptions,
             bool? enableImages,
             bool? enableUserData,
             int? imageTypeLimit,
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 2ff40a8a5e..e2a0cf4faf 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -73,5 +74,22 @@ namespace Jellyfin.Api.Helpers
 
             return session;
         }
+
+        /// <summary>
+        /// Get Guid array from string.
+        /// </summary>
+        /// <param name="value">String value.</param>
+        /// <returns>Guid array.</returns>
+        internal static Guid[] GetGuids(string? value)
+        {
+            if (value == null)
+            {
+                return Array.Empty<Guid>();
+            }
+
+            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(i => new Guid(i))
+                .ToArray();
+        }
     }
 }

From 472fd5217f25b6849ee4c1de7da92c70b5c1a9b1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 16:07:09 -0600
Subject: [PATCH 228/463] clean up

---
 Jellyfin.Api/Controllers/PlaylistsController.cs | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 9e2a91e102..2e3f6c54af 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,7 +1,4 @@
-#nullable enable
-#pragma warning disable CA1801
-
-using System;
+using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Extensions;
@@ -124,7 +121,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds)
         {
-            _playlistManager.RemoveFromPlaylist(playlistId, entryIds.Split(','));
+            _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
             return NoContent();
         }
 

From f017f5c97fb091304bba819e9ba73510cf85a9b1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 16:07:53 -0600
Subject: [PATCH 229/463] clean up

---
 Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 20835eecbd..0d67c86f71 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,5 +1,4 @@
-#nullable enable
-using System;
+using System;
 
 namespace Jellyfin.Api.Models.PlaylistDtos
 {

From 9a8deadc215aa1ca25e1667c8c373a13e07d301e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 17:06:33 -0600
Subject: [PATCH 230/463] implement all non image get endpoints

---
 Jellyfin.Api/Controllers/ImageController.cs | 242 +++++++++++++++++-
 MediaBrowser.Api/Images/ImageService.cs     | 261 --------------------
 2 files changed, 236 insertions(+), 267 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 6742bffc61..d8c67dbea0 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,15 +1,24 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
 
@@ -69,13 +78,18 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Users/{userId}/Images/{imageType}")]
-        [HttpPost("/Users/{userId}/Images/{imageType}/{index}")]
+        [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> PostUserImage(
             [FromRoute] Guid userId,
             [FromRoute] ImageType imageType,
-            [FromRoute] int? index)
+            [FromRoute] int? index = null)
         {
-            // TODO AssertCanUpdateUser(_authContext, _userManager, id, true);
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+            {
+                return Forbid("User is not allowed to update the image.");
+            }
 
             var user = _userManager.GetUserById(userId);
             await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
@@ -102,13 +116,19 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image deleted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("/Users/{userId}/Images/{itemType}")]
-        [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")]
+        [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DeleteUserImage(
             [FromRoute] Guid userId,
             [FromRoute] ImageType imageType,
-            [FromRoute] int? index)
+            [FromRoute] int? index = null)
         {
-            // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true);
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+            {
+                return Forbid("User is not allowed to delete the image.");
+            }
 
             var user = _userManager.GetUserById(userId);
             try
@@ -124,6 +144,164 @@ namespace Jellyfin.Api.Controllers
             return NoContent();
         }
 
+        /// <summary>
+        /// Delete an item's image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">The image index.</param>
+        /// <response code="204">Image deleted.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpDelete("/Items/{itemId}/Images/{imageType}")]
+        [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult DeleteItemImage(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            item.DeleteImage(imageType, imageIndex ?? 0);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Set item image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">(Unused) Image index.</param>
+        /// <response code="204">Image saved.</response>
+        /// <response code="400">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpPost("/Items/{itemId}/Images/{imageType}")]
+        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+        public async Task<ActionResult> SetItemImage(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            // Handle image/png; charset=utf-8
+            var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+            await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+            item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates the index for an item image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Old image index.</param>
+        /// <param name="newIndex">New image index.</param>
+        /// <response code="204">Image index updated.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult UpdateItemImageIndex(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int imageIndex,
+            [FromQuery] int newIndex)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            item.SwapImages(imageType, imageIndex, newIndex);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Get item image infos.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Item images returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpGet("/Items/{itemId}/Images")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            var list = new List<ImageInfo>();
+            var itemImages = item.ImageInfos;
+
+            if (itemImages.Length == 0)
+            {
+                // short-circuit
+                return list;
+            }
+
+            _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
+
+            foreach (var image in itemImages)
+            {
+                if (!item.AllowsMultipleImages(image.Type))
+                {
+                    var info = GetImageInfo(item, image, null);
+
+                    if (info != null)
+                    {
+                        list.Add(info);
+                    }
+                }
+            }
+
+            foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
+            {
+                var index = 0;
+
+                // Prevent implicitly captured closure
+                var currentImageType = imageType;
+
+                foreach (var image in itemImages.Where(i => i.Type == currentImageType))
+                {
+                    var info = GetImageInfo(item, image, index);
+
+                    if (info != null)
+                    {
+                        list.Add(info);
+                    }
+
+                    index++;
+                }
+            }
+
+            return list;
+        }
+
         private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
         {
             using var reader = new StreamReader(inputStream);
@@ -135,5 +313,57 @@ namespace Jellyfin.Api.Controllers
                 Position = 0
             };
         }
+
+        private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
+        {
+            int? width = null;
+            int? height = null;
+            string? blurhash = null;
+            long length = 0;
+
+            try
+            {
+                if (info.IsLocalFile)
+                {
+                    var fileInfo = _fileSystem.GetFileInfo(info.Path);
+                    length = fileInfo.Length;
+
+                    blurhash = info.BlurHash;
+                    width = info.Width;
+                    height = info.Height;
+
+                    if (width <= 0 || height <= 0)
+                    {
+                        width = null;
+                        height = null;
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
+            }
+
+            try
+            {
+                return new ImageInfo
+                {
+                    Path = info.Path,
+                    ImageIndex = imageIndex,
+                    ImageType = info.Type,
+                    ImageTag = _imageProcessor.GetImageCacheTag(item, info),
+                    Size = length,
+                    BlurHash = blurhash,
+                    Width = width,
+                    Height = height
+                };
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
+
+                return null;
+            }
+        }
     }
 }
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index 48c879bb72..a98266e0d2 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
@@ -15,7 +14,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
@@ -26,21 +24,6 @@ using User = Jellyfin.Data.Entities.User;
 
 namespace MediaBrowser.Api.Images
 {
-    /// <summary>
-    /// Class GetItemImage.
-    /// </summary>
-    [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
-    [Authenticated]
-    public class GetItemImageInfos : IReturn<List<ImageInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
     [Route("/Items/{Id}/Images/{Type}", "GET")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
     [Route("/Items/{Id}/Images/{Type}", "HEAD")]
@@ -57,42 +40,6 @@ namespace MediaBrowser.Api.Images
         public Guid Id { get; set; }
     }
 
-    /// <summary>
-    /// Class UpdateItemImageIndex
-    /// </summary>
-    [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")]
-    [Authenticated(Roles = "admin")]
-    public class UpdateItemImageIndex : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the image.
-        /// </summary>
-        /// <value>The type of the image.</value>
-        [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public ImageType Type { get; set; }
-
-        /// <summary>
-        /// Gets or sets the index.
-        /// </summary>
-        /// <value>The index.</value>
-        [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int Index { get; set; }
-
-        /// <summary>
-        /// Gets or sets the new index.
-        /// </summary>
-        /// <value>The new index.</value>
-        [ApiMember(Name = "NewIndex", Description = "The new image index", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public int NewIndex { get; set; }
-    }
-
     /// <summary>
     /// Class GetPersonImage
     /// </summary>
@@ -147,44 +94,6 @@ namespace MediaBrowser.Api.Images
         public Guid Id { get; set; }
     }
 
-    /// <summary>
-    /// Class DeleteItemImage
-    /// </summary>
-    [Route("/Items/{Id}/Images/{Type}", "DELETE")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
-    [Authenticated(Roles = "admin")]
-    public class DeleteItemImage : DeleteImageRequest, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class PostItemImage
-    /// </summary>
-    [Route("/Items/{Id}/Images/{Type}", "POST")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
-    [Authenticated(Roles = "admin")]
-    public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The raw Http Request Input Stream
-        /// </summary>
-        /// <value>The request stream.</value>
-        public Stream RequestStream { get; set; }
-    }
-
     /// <summary>
     /// Class ImageService
     /// </summary>
@@ -223,126 +132,6 @@ namespace MediaBrowser.Api.Images
             _authContext = authContext;
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItemImageInfos request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var result = GetItemImageInfos(item);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item image infos.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>Task{List{ImageInfo}}.</returns>
-        public List<ImageInfo> GetItemImageInfos(BaseItem item)
-        {
-            var list = new List<ImageInfo>();
-            var itemImages = item.ImageInfos;
-
-            if (itemImages.Length == 0)
-            {
-                // short-circuit
-                return list;
-            }
-
-            _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
-
-            foreach (var image in itemImages)
-            {
-                if (!item.AllowsMultipleImages(image.Type))
-                {
-                    var info = GetImageInfo(item, image, null);
-
-                    if (info != null)
-                    {
-                        list.Add(info);
-                    }
-                }
-            }
-
-            foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
-            {
-                var index = 0;
-
-                // Prevent implicitly captured closure
-                var currentImageType = imageType;
-
-                foreach (var image in itemImages.Where(i => i.Type == currentImageType))
-                {
-                    var info = GetImageInfo(item, image, index);
-
-                    if (info != null)
-                    {
-                        list.Add(info);
-                    }
-
-                    index++;
-                }
-            }
-
-            return list;
-        }
-
-        private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
-        {
-            int? width = null;
-            int? height = null;
-            string blurhash = null;
-            long length = 0;
-
-            try
-            {
-                if (info.IsLocalFile)
-                {
-                    var fileInfo = _fileSystem.GetFileInfo(info.Path);
-                    length = fileInfo.Length;
-
-                    blurhash = info.BlurHash;
-                    width = info.Width;
-                    height = info.Height;
-
-                    if (width <= 0 || height <= 0)
-                    {
-                        width = null;
-                        height = null;
-                    }
-                }
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error getting image information for {Item}", item.Name);
-            }
-
-            try
-            {
-                return new ImageInfo
-                {
-                    Path = info.Path,
-                    ImageIndex = imageIndex,
-                    ImageType = info.Type,
-                    ImageTag = _imageProcessor.GetImageCacheTag(item, info),
-                    Size = length,
-                    BlurHash = blurhash,
-                    Width = width,
-                    Height = height
-                };
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error getting image information for {Path}", info.Path);
-
-                return null;
-            }
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -400,56 +189,6 @@ namespace MediaBrowser.Api.Images
             return GetImage(request, item.Id, item, true);
         }
 
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(PostItemImage request)
-        {
-            var id = Guid.Parse(GetPathValue(1));
-
-            request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
-
-            var item = _libraryManager.GetItemById(id);
-
-            return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteItemImage request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            item.DeleteImage(request.Type, request.Index ?? 0);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateItemImageIndex request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            UpdateItemIndex(item, request.Type, request.Index, request.NewIndex);
-        }
-
-        /// <summary>
-        /// Updates the index of the item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="currentIndex">Index of the current.</param>
-        /// <param name="newIndex">The new index.</param>
-        /// <returns>Task.</returns>
-        private void UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex)
-        {
-            item.SwapImages(type, currentIndex, newIndex);
-        }
-
         /// <summary>
         /// Gets the image.
         /// </summary>

From 10ddbc34ecfc5542f3b32fe3cc4740e30b62cccd Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 20 Jun 2020 18:02:07 -0600
Subject: [PATCH 231/463] Add missing attributes, fix response codes, fix route
 parameter casing

---
 .../Controllers/ConfigurationController.cs    |  4 +-
 Jellyfin.Api/Controllers/DevicesController.cs |  1 +
 .../DisplayPreferencesController.cs           | 22 ++----
 .../Controllers/ImageByNameController.cs      |  6 +-
 .../Controllers/ItemRefreshController.cs      | 15 ++--
 .../Controllers/NotificationsController.cs    |  8 +-
 Jellyfin.Api/Controllers/PackageController.cs | 19 ++---
 Jellyfin.Api/Controllers/PluginsController.cs | 28 +++++--
 .../Controllers/RemoteImageController.cs      | 30 ++++----
 Jellyfin.Api/Controllers/SessionController.cs | 72 +++++++++---------
 .../Controllers/SubtitleController.cs         | 54 +++++++-------
 Jellyfin.Api/Controllers/UserController.cs    | 74 +++++++++----------
 .../Controllers/VideoAttachmentsController.cs |  2 +-
 13 files changed, 172 insertions(+), 163 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 74f1677bdb..d275ed2eba 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="key">Configuration key.</param>
         /// <response code="200">Configuration returned.</response>
         /// <returns>Configuration.</returns>
-        [HttpGet("Configuration/{Key}")]
+        [HttpGet("Configuration/{key}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
         {
@@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="key">Configuration key.</param>
         /// <response code="204">Named configuration updated.</response>
         /// <returns>Update status.</returns>
-        [HttpPost("Configuration/{Key}")]
+        [HttpPost("Configuration/{key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 78368eed60..55ca7b7c0f 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -133,6 +133,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
         {
             var existingDevice = _deviceManager.GetDevice(id);
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 697a0baf42..56ac215a96 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
 using System.Threading;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
@@ -34,9 +35,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="client">Client.</param>
         /// <response code="200">Display preferences retrieved.</response>
         /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
-        [HttpGet("{DisplayPreferencesId}")]
+        [HttpGet("{displayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<DisplayPreferences> GetDisplayPreferences(
             [FromRoute] string displayPreferencesId,
             [FromQuery] [Required] string userId,
@@ -52,30 +52,24 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User Id.</param>
         /// <param name="client">Client.</param>
         /// <param name="displayPreferences">New Display Preferences object.</param>
-        /// <response code="200">Display preferences updated.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
-        [HttpPost("{DisplayPreferencesId}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        [ProducesResponseType(typeof(ModelStateDictionary), StatusCodes.Status400BadRequest)]
-        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        /// <response code="204">Display preferences updated.</response>
+        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        [HttpPost("{displayPreferencesId}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
         public ActionResult UpdateDisplayPreferences(
             [FromRoute] string displayPreferencesId,
             [FromQuery, BindRequired] string userId,
             [FromQuery, BindRequired] string client,
             [FromBody, BindRequired] DisplayPreferences displayPreferences)
         {
-            if (displayPreferencesId == null)
-            {
-                // TODO - refactor so parameter doesn't exist or is actually used.
-            }
-
             _displayPreferencesRepository.SaveDisplayPreferences(
                 displayPreferences,
                 userId,
                 client,
                 CancellationToken.None);
 
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 70f46ffa49..0e3c32d3cc 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Image stream retrieved.</response>
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("General/{Name}/{Type}")]
+        [HttpGet("General/{name}/{type}")]
         [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -103,7 +103,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Image stream retrieved.</response>
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("Ratings/{Theme}/{Name}")]
+        [HttpGet("Ratings/{theme}/{name}")]
         [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -136,7 +136,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Image stream retrieved.</response>
         /// <response code="404">Image not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        [HttpGet("MediaInfo/{Theme}/{Name}")]
+        [HttpGet("MediaInfo/{theme}/{name}")]
         [AllowAnonymous]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 6a16a89c5a..f10f9fb3d8 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,3 +1,4 @@
+using System;
 using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Controller.Library;
@@ -40,29 +41,29 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Refreshes metadata for an item.
         /// </summary>
-        /// <param name="id">Item id.</param>
+        /// <param name="itemId">Item id.</param>
         /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
         /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
         /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
         /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
         /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
-        /// <response code="200">Item metadata refresh queued.</response>
+        /// <response code="204">Item metadata refresh queued.</response>
         /// <response code="404">Item to refresh not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
-        [HttpPost("{Id}/Refresh")]
+        [HttpPost("{itemId}/Refresh")]
         [Description("Refreshes metadata for an item.")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")]
         public ActionResult Post(
-            [FromRoute] string id,
+            [FromRoute] Guid itemId,
             [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
             [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
             [FromQuery] bool replaceAllMetadata = false,
             [FromQuery] bool replaceAllImages = false,
             [FromQuery] bool recursive = false)
         {
-            var item = _libraryManager.GetItemById(id);
+            var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             {
                 return NotFound();
@@ -82,7 +83,7 @@ namespace Jellyfin.Api.Controllers
             };
 
             _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
-            return Ok();
+            return NoContent();
         }
     }
 }
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 01dd23c77f..f226364894 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -42,7 +42,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <response code="200">Notifications returned.</response>
         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
-        [HttpGet("{UserID}")]
+        [HttpGet("{userId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")]
@@ -63,7 +63,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user's ID.</param>
         /// <response code="200">Summary of user's notifications returned.</response>
         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
-        [HttpGet("{UserID}/Summary")]
+        [HttpGet("{userId}/Summary")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
         /// <response code="204">Notifications set as read.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
-        [HttpPost("{UserID}/Read")]
+        [HttpPost("{userId}/Read")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
@@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
         /// <response code="204">Notifications set as unread.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
-        [HttpPost("{UserID}/Unread")]
+        [HttpPost("{userId}/Unread")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 943c23f8e9..486575d23a 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -35,9 +35,10 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="name">The name of the package.</param>
         /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
+        /// <response code="200">Package retrieved.</response>
         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
-        [HttpGet("/{Name}")]
-        [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)]
+        [HttpGet("/{name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PackageInfo>> GetPackageInfo(
             [FromRoute] [Required] string name,
             [FromQuery] string? assemblyGuid)
@@ -54,9 +55,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available packages.
         /// </summary>
+        /// <response code="200">Available packages returned.</response>
         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
         [HttpGet]
-        [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<IEnumerable<PackageInfo>> GetPackages()
         {
             IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
@@ -73,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Package found.</response>
         /// <response code="404">Package not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
-        [HttpPost("/Installed/{Name}")]
+        [HttpPost("/Installed/{name}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Authorize(Policy = Policies.RequiresElevation)]
@@ -102,17 +104,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Cancels a package installation.
         /// </summary>
-        /// <param name="id">Installation Id.</param>
+        /// <param name="packageId">Installation Id.</param>
         /// <response code="204">Installation cancelled.</response>
         /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
-        [HttpDelete("/Installing/{id}")]
+        [HttpDelete("/Installing/{packageId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public IActionResult CancelPackageInstallation(
-            [FromRoute] [Required] string id)
+            [FromRoute] [Required] Guid packageId)
         {
-            _installationManager.CancelInstallation(new Guid(id));
-
+            _installationManager.CancelInstallation(packageId);
             return NoContent();
         }
     }
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 6075544cf7..8a0913307e 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
@@ -45,6 +46,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Installed plugins returned.</response>
         /// <returns>List of currently installed plugins.</returns>
         [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
         {
@@ -55,11 +57,13 @@ namespace Jellyfin.Api.Controllers
         /// Uninstalls a plugin.
         /// </summary>
         /// <param name="pluginId">Plugin id.</param>
-        /// <response code="200">Plugin uninstalled.</response>
+        /// <response code="204">Plugin uninstalled.</response>
         /// <response code="404">Plugin not found.</response>
         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpDelete("{pluginId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
         {
             var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
@@ -69,7 +73,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             _installationManager.UninstallPlugin(plugin);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -80,6 +84,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Plugin not found or plugin configuration not found.</response>
         /// <returns>Plugin configuration.</returns>
         [HttpGet("{pluginId}/Configuration")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId)
         {
             if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
@@ -97,14 +103,16 @@ namespace Jellyfin.Api.Controllers
         /// Accepts plugin configuration as JSON body.
         /// </remarks>
         /// <param name="pluginId">Plugin id.</param>
-        /// <response code="200">Plugin configuration updated.</response>
-        /// <response code="200">Plugin not found or plugin does not have configuration.</response>
+        /// <response code="204">Plugin configuration updated.</response>
+        /// <response code="404">Plugin not found or plugin does not have configuration.</response>
         /// <returns>
         /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
         ///    The task result contains an <see cref="OkResult"/> indicating success, or <see cref="NotFoundResult"/>
         ///    when plugin not found or plugin doesn't have configuration.
         /// </returns>
         [HttpPost("{pluginId}/Configuration")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
         {
             if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
@@ -116,7 +124,7 @@ namespace Jellyfin.Api.Controllers
                 .ConfigureAwait(false);
 
             plugin.UpdateConfiguration(configuration);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -126,6 +134,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Plugin security info.</returns>
         [Obsolete("This endpoint should not be used.")]
         [HttpGet("SecurityInfo")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
         {
             return new PluginSecurityInfo
@@ -139,14 +148,15 @@ namespace Jellyfin.Api.Controllers
         /// Updates plugin security info.
         /// </summary>
         /// <param name="pluginSecurityInfo">Plugin security info.</param>
-        /// <response code="200">Plugin security info updated.</response>
-        /// <returns>An <see cref="OkResult"/>.</returns>
+        /// <response code="204">Plugin security info updated.</response>
+        /// <returns>An <see cref="NoContentResult"/>.</returns>
         [Obsolete("This endpoint should not be used.")]
         [HttpPost("SecurityInfo")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
         {
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -157,6 +167,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Mb registration record.</returns>
         [Obsolete("This endpoint should not be used.")]
         [HttpPost("RegistrationRecords/{name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name)
         {
             return new MBRegistrationRecord
@@ -178,6 +189,7 @@ namespace Jellyfin.Api.Controllers
         /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
         [Obsolete("Paid plugins are not supported")]
         [HttpGet("/Registrations/{name}")]
+        [ProducesResponseType(StatusCodes.Status501NotImplemented)]
         public ActionResult GetRegistration([FromRoute] string name)
         {
             // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 80983ee649..41b7f98ee1 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available remote images for an item.
         /// </summary>
-        /// <param name="id">Item Id.</param>
+        /// <param name="itemId">Item Id.</param>
         /// <param name="type">The image type.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
@@ -64,18 +64,18 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Remote Images returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>Remote Image Result.</returns>
-        [HttpGet("{Id}/RemoteImages")]
+        [HttpGet("{itemId}/RemoteImages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
-            [FromRoute] string id,
+            [FromRoute] Guid itemId,
             [FromQuery] ImageType? type,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string providerName,
             [FromQuery] bool includeAllLanguages)
         {
-            var item = _libraryManager.GetItemById(id);
+            var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             {
                 return NotFound();
@@ -123,16 +123,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets available remote image providers for an item.
         /// </summary>
-        /// <param name="id">Item Id.</param>
+        /// <param name="itemId">Item Id.</param>
         /// <response code="200">Returned remote image providers.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>List of remote image providers.</returns>
-        [HttpGet("{Id}/RemoteImages/Providers")]
+        [HttpGet("{itemId}/RemoteImages/Providers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] string id)
+        public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId)
         {
-            var item = _libraryManager.GetItemById(id);
+            var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             {
                 return NotFound();
@@ -195,21 +195,21 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Downloads a remote image for an item.
         /// </summary>
-        /// <param name="id">Item Id.</param>
+        /// <param name="itemId">Item Id.</param>
         /// <param name="type">The image type.</param>
         /// <param name="imageUrl">The image url.</param>
-        /// <response code="200">Remote image downloaded.</response>
+        /// <response code="204">Remote image downloaded.</response>
         /// <response code="404">Remote image not found.</response>
         /// <returns>Download status.</returns>
-        [HttpPost("{Id}/RemoteImages/Download")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [HttpPost("{itemId}/RemoteImages/Download")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> DownloadRemoteImage(
-            [FromRoute] string id,
+            [FromRoute] Guid itemId,
             [FromQuery, BindRequired] ImageType type,
             [FromQuery] string imageUrl)
         {
-            var item = _libraryManager.GetItemById(id);
+            var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             {
                 return NotFound();
@@ -219,7 +219,7 @@ namespace Jellyfin.Api.Controllers
                 .ConfigureAwait(false);
 
             item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 4f259536a1..315bc9728b 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -113,16 +113,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Instructs a session to browse to an item or view.
         /// </summary>
-        /// <param name="id">The session Id.</param>
+        /// <param name="sessionId">The session Id.</param>
         /// <param name="itemType">The type of item to browse to.</param>
         /// <param name="itemId">The Id of the item.</param>
         /// <param name="itemName">The name of the item.</param>
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Viewing")]
+        [HttpPost("/Sessions/{sessionId}/Viewing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DisplayContent(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromQuery] string itemType,
             [FromQuery] string itemId,
             [FromQuery] string itemName)
@@ -136,7 +136,7 @@ namespace Jellyfin.Api.Controllers
 
             _sessionManager.SendBrowseCommand(
                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
-                id,
+                sessionId,
                 command,
                 CancellationToken.None);
 
@@ -146,17 +146,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Instructs a session to play an item.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="itemIds">The ids of the items to play, comma delimited.</param>
         /// <param name="startPositionTicks">The starting position of the first item.</param>
         /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
         /// <param name="playRequest">The <see cref="PlayRequest"/>.</param>
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Playing")]
+        [HttpPost("/Sessions/{sessionId}/Playing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromQuery] Guid[] itemIds,
             [FromQuery] long? startPositionTicks,
             [FromQuery] PlayCommand playCommand,
@@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
 
             _sessionManager.SendPlayCommand(
                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
-                id,
+                sessionId,
                 playRequest,
                 CancellationToken.None);
 
@@ -183,19 +183,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Issues a playstate command to a client.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
         /// <response code="204">Playstate command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Playing/{command}")]
+        [HttpPost("/Sessions/{sessionId}/Playing/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromBody] PlaystateRequest playstateRequest)
         {
             _sessionManager.SendPlaystateCommand(
                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
-                id,
+                sessionId,
                 playstateRequest,
                 CancellationToken.None);
 
@@ -205,14 +205,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Issues a system command to a client.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="command">The command to send.</param>
         /// <response code="204">System command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/System/{Command}")]
+        [HttpPost("/Sessions/{sessionId}/System/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromRoute] string command)
         {
             var name = command;
@@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers
                 ControllingUserId = currentSession.UserId
             };
 
-            _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None);
+            _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
 
             return NoContent();
         }
@@ -236,14 +236,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Issues a general command to a client.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="command">The command to send.</param>
         /// <response code="204">General command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Command/{Command}")]
+        [HttpPost("/Sessions/{sessionId}/Command/{Command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromRoute] string command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
                 ControllingUserId = currentSession.UserId
             };
 
-            _sessionManager.SendGeneralCommand(currentSession.Id, id, generalCommand, CancellationToken.None);
+            _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
 
             return NoContent();
         }
@@ -262,14 +262,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Issues a full general command to a client.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="command">The <see cref="GeneralCommand"/>.</param>
         /// <response code="204">Full general command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Command")]
+        [HttpPost("/Sessions/{sessionId}/Command")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendFullGeneralCommand(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromBody, Required] GeneralCommand command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -283,7 +283,7 @@ namespace Jellyfin.Api.Controllers
 
             _sessionManager.SendGeneralCommand(
                 currentSession.Id,
-                id,
+                sessionId,
                 command,
                 CancellationToken.None);
 
@@ -293,16 +293,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Issues a command to a client to display a message to the user.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="text">The message test.</param>
         /// <param name="header">The message header.</param>
         /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
         /// <response code="204">Message sent.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/Message")]
+        [HttpPost("/Sessions/{sessionId}/Message")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendMessageCommand(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromQuery] string text,
             [FromQuery] string header,
             [FromQuery] long? timeoutMs)
@@ -314,7 +314,7 @@ namespace Jellyfin.Api.Controllers
                 Text = text
             };
 
-            _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, id, command, CancellationToken.None);
+            _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
 
             return NoContent();
         }
@@ -322,34 +322,34 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Adds an additional user to a session.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="userId">The user id.</param>
         /// <response code="204">User added to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{id}/User/{userId}")]
+        [HttpPost("/Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddUserToSession(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromRoute] Guid userId)
         {
-            _sessionManager.AddAdditionalUser(id, userId);
+            _sessionManager.AddAdditionalUser(sessionId, userId);
             return NoContent();
         }
 
         /// <summary>
         /// Removes an additional user from a session.
         /// </summary>
-        /// <param name="id">The session id.</param>
+        /// <param name="sessionId">The session id.</param>
         /// <param name="userId">The user id.</param>
         /// <response code="204">User removed from session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Sessions/{id}/User/{userId}")]
+        [HttpDelete("/Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveUserFromSession(
-            [FromRoute] string id,
+            [FromRoute] string sessionId,
             [FromRoute] Guid userId)
         {
-            _sessionManager.RemoveAdditionalUser(id, userId);
+            _sessionManager.RemoveAdditionalUser(sessionId, userId);
             return NoContent();
         }
 
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 74ec5f9b52..95cc39524c 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -75,20 +75,20 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Deletes an external subtitle file.
         /// </summary>
-        /// <param name="id">The item id.</param>
+        /// <param name="itemId">The item id.</param>
         /// <param name="index">The index of the subtitle file.</param>
         /// <response code="204">Subtitle deleted.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Videos/{id}/Subtitles/{index}")]
+        [HttpDelete("/Videos/{itemId}/Subtitles/{index}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<Task> DeleteSubtitle(
-            [FromRoute] Guid id,
+            [FromRoute] Guid itemId,
             [FromRoute] int index)
         {
-            var item = _libraryManager.GetItemById(id);
+            var item = _libraryManager.GetItemById(itemId);
 
             if (item == null)
             {
@@ -102,20 +102,20 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Search remote subtitles.
         /// </summary>
-        /// <param name="id">The item id.</param>
+        /// <param name="itemId">The item id.</param>
         /// <param name="language">The language of the subtitles.</param>
         /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
         /// <response code="200">Subtitles retrieved.</response>
         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
-        [HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
+        [HttpGet("/Items/{itemId}/RemoteSearch/Subtitles/{language}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
-            [FromRoute] Guid id,
+            [FromRoute] Guid itemId,
             [FromRoute] string language,
             [FromQuery] bool? isPerfectMatch)
         {
-            var video = (Video)_libraryManager.GetItemById(id);
+            var video = (Video)_libraryManager.GetItemById(itemId);
 
             return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
         }
@@ -123,18 +123,18 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Downloads a remote subtitle.
         /// </summary>
-        /// <param name="id">The item id.</param>
+        /// <param name="itemId">The item id.</param>
         /// <param name="subtitleId">The subtitle id.</param>
         /// <response code="204">Subtitle downloaded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
+        [HttpPost("/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
-            [FromRoute] Guid id,
+            [FromRoute] Guid itemId,
             [FromRoute] string subtitleId)
         {
-            var video = (Video)_libraryManager.GetItemById(id);
+            var video = (Video)_libraryManager.GetItemById(itemId);
 
             try
             {
@@ -171,28 +171,28 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets subtitles in a specified format.
         /// </summary>
-        /// <param name="id">The item id.</param>
+        /// <param name="itemId">The item id.</param>
         /// <param name="mediaSourceId">The media source id.</param>
         /// <param name="index">The subtitle stream index.</param>
         /// <param name="format">The format of the returned subtitle.</param>
-        /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
         /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
         /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
         /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
+        /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
-        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
-        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
+        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitle(
-            [FromRoute, Required] Guid id,
+            [FromRoute, Required] Guid itemId,
             [FromRoute, Required] string mediaSourceId,
             [FromRoute, Required] int index,
             [FromRoute, Required] string format,
-            [FromRoute] long startPositionTicks,
             [FromQuery] long? endPositionTicks,
             [FromQuery] bool copyTimestamps,
-            [FromQuery] bool addVttTimeMap)
+            [FromQuery] bool addVttTimeMap,
+            [FromRoute] long startPositionTicks = 0)
         {
             if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
             {
@@ -201,9 +201,9 @@ namespace Jellyfin.Api.Controllers
 
             if (string.IsNullOrEmpty(format))
             {
-                var item = (Video)_libraryManager.GetItemById(id);
+                var item = (Video)_libraryManager.GetItemById(itemId);
 
-                var idString = id.ToString("N", CultureInfo.InvariantCulture);
+                var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
                 var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
                     .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
 
@@ -216,7 +216,7 @@ namespace Jellyfin.Api.Controllers
 
             if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
             {
-                await using Stream stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+                await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
                 using var reader = new StreamReader(stream);
 
                 var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers
 
             return File(
                 await EncodeSubtitles(
-                    id,
+                    itemId,
                     mediaSourceId,
                     index,
                     format,
@@ -241,23 +241,23 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets an HLS subtitle playlist.
         /// </summary>
-        /// <param name="id">The item id.</param>
+        /// <param name="itemId">The item id.</param>
         /// <param name="index">The subtitle stream index.</param>
         /// <param name="mediaSourceId">The media source id.</param>
         /// <param name="segmentLength">The subtitle segment length.</param>
         /// <response code="200">Subtitle playlist retrieved.</response>
         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
-        [HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
+        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> GetSubtitlePlaylist(
-            [FromRoute] Guid id,
+            [FromRoute] Guid itemId,
             [FromRoute] int index,
             [FromRoute] string mediaSourceId,
             [FromQuery, Required] int segmentLength)
         {
-            var item = (Video)_libraryManager.GetItemById(id);
+            var item = (Video)_libraryManager.GetItemById(itemId);
 
             var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
 
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 68ab5813ce..0d57dcc837 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -105,17 +105,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a user by Id.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <response code="200">User returned.</response>
         /// <response code="404">User not found.</response>
         /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
-        [HttpGet("{id}")]
+        [HttpGet("{userId}")]
         [Authorize(Policy = Policies.IgnoreSchedule)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<UserDto> GetUserById([FromRoute] Guid id)
+        public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
         {
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (user == null)
             {
@@ -129,17 +129,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Deletes a user.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <response code="200">User deleted.</response>
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
-        [HttpDelete("{id}")]
+        [HttpDelete("{userId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult DeleteUser([FromRoute] Guid id)
+        public ActionResult DeleteUser([FromRoute] Guid userId)
         {
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (user == null)
             {
@@ -154,23 +154,23 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Authenticates a user.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="pw">The password as plain text.</param>
         /// <param name="password">The password sha1-hash.</param>
         /// <response code="200">User authenticated.</response>
         /// <response code="403">Sha1-hashed password only is not allowed.</response>
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
-        [HttpPost("{id}/Authenticate")]
+        [HttpPost("{userId}/Authenticate")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
-            [FromRoute, Required] Guid id,
+            [FromRoute, Required] Guid userId,
             [FromQuery, BindRequired] string pw,
             [FromQuery, BindRequired] string password)
         {
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (user == null)
             {
@@ -230,27 +230,27 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user's password.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
         /// <response code="200">Password successfully reset.</response>
         /// <response code="403">User is not allowed to update the password.</response>
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
-        [HttpPost("{id}/Password")]
+        [HttpPost("{userId}/Password")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> UpdateUserPassword(
-            [FromRoute] Guid id,
+            [FromRoute] Guid userId,
             [FromBody] UpdateUserPassword request)
         {
-            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
             {
                 return Forbid("User is not allowed to update the password.");
             }
 
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (user == null)
             {
@@ -288,27 +288,27 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user's easy password.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
         /// <response code="200">Password successfully reset.</response>
         /// <response code="403">User is not allowed to update the password.</response>
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
-        [HttpPost("{id}/EasyPassword")]
+        [HttpPost("{userId}/EasyPassword")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateUserEasyPassword(
-            [FromRoute] Guid id,
+            [FromRoute] Guid userId,
             [FromBody] UpdateUserEasyPassword request)
         {
-            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true))
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
             {
                 return Forbid("User is not allowed to update the easy password.");
             }
 
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (user == null)
             {
@@ -330,19 +330,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="updateUser">The updated user model.</param>
         /// <response code="204">User updated.</response>
         /// <response code="400">User information was not supplied.</response>
         /// <response code="403">User update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
-        [HttpPost("{id}")]
+        [HttpPost("{userId}")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         public async Task<ActionResult> UpdateUser(
-            [FromRoute] Guid id,
+            [FromRoute] Guid userId,
             [FromBody] UserDto updateUser)
         {
             if (updateUser == null)
@@ -350,12 +350,12 @@ namespace Jellyfin.Api.Controllers
                 return BadRequest();
             }
 
-            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false))
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
             {
                 return Forbid("User update not allowed.");
             }
 
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
             {
@@ -374,19 +374,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user policy.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="newPolicy">The new user policy.</param>
         /// <response code="204">User policy updated.</response>
         /// <response code="400">User policy was not supplied.</response>
         /// <response code="403">User policy update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
-        [HttpPost("{id}/Policy")]
+        [HttpPost("{userId}/Policy")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         public ActionResult UpdateUserPolicy(
-            [FromRoute] Guid id,
+            [FromRoute] Guid userId,
             [FromBody] UserPolicy newPolicy)
         {
             if (newPolicy == null)
@@ -394,7 +394,7 @@ namespace Jellyfin.Api.Controllers
                 return BadRequest();
             }
 
-            var user = _userManager.GetUserById(id);
+            var user = _userManager.GetUserById(userId);
 
             // If removing admin access
             if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)))
@@ -423,7 +423,7 @@ namespace Jellyfin.Api.Controllers
                 _sessionManager.RevokeUserTokens(user.Id, currentToken);
             }
 
-            _userManager.UpdatePolicy(id, newPolicy);
+            _userManager.UpdatePolicy(userId, newPolicy);
 
             return NoContent();
         }
@@ -431,25 +431,25 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a user configuration.
         /// </summary>
-        /// <param name="id">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="userConfig">The new user configuration.</param>
         /// <response code="204">User configuration updated.</response>
         /// <response code="403">User configuration update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost("{id}/Configuration")]
+        [HttpPost("{userId}/Configuration")]
         [Authorize]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         public ActionResult UpdateUserConfiguration(
-            [FromRoute] Guid id,
+            [FromRoute] Guid userId,
             [FromBody] UserConfiguration userConfig)
         {
-            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false))
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
             {
                 return Forbid("User configuration update not allowed");
             }
 
-            _userManager.UpdateConfiguration(id, userConfig);
+            _userManager.UpdateConfiguration(userId, userConfig);
 
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 2528fd75d0..943ba8af3d 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Attachment retrieved.</response>
         /// <response code="404">Video or attachment not found.</response>
         /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
-        [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")]
+        [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]

From 9a223b7359305ab718b744394688e1d948b56686 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 21 Jun 2020 12:35:06 +0200
Subject: [PATCH 232/463] Fix suggestions

---
 Jellyfin.Api/Controllers/DashboardController.cs | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 6a7bf7d0aa..21c320a490 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
 
             configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
 
-            if (pageType != null)
+            if (pageType.HasValue)
             {
                 configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList();
             }
@@ -246,14 +246,12 @@ namespace Jellyfin.Api.Controllers
 
         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
         {
-            var hasConfig = plugin as IHasWebPages;
-
-            if (hasConfig == null)
+            if (!(plugin is IHasWebPages))
             {
                 return new List<Tuple<PluginPageInfo, IPlugin>>();
             }
 
-            return hasConfig.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
+            return (plugin as IHasWebPages)!.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
         }
 
         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()

From 8f9c9859882815d10e51ad5c2116d516a1cb89f4 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 21 Jun 2020 16:00:16 +0200
Subject: [PATCH 233/463] Move VideosService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/VideosController.cs | 202 +++++++++++++++++++
 MediaBrowser.Api/VideosService.cs            | 193 ------------------
 2 files changed, 202 insertions(+), 193 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/VideosController.cs
 delete mode 100644 MediaBrowser.Api/VideosService.cs

diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
new file mode 100644
index 0000000000..532ce59c50
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The videos controller.
+    /// </summary>
+    [Route("Videos")]
+    public class VideosController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="VideosController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public VideosController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets additional parts for a video.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Additional parts returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
+        [HttpGet("{itemId}/AdditionalParts")]
+        [Authorize]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        {
+            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+
+            var item = itemId.Equals(Guid.Empty)
+                ? (!userId.Equals(Guid.Empty)
+                    ? _libraryManager.GetUserRootFolder()
+                    : _libraryManager.RootFolder)
+                : _libraryManager.GetItemById(itemId);
+
+            var dtoOptions = new DtoOptions();
+            dtoOptions = dtoOptions.AddClientFields(Request);
+
+            BaseItemDto[] items;
+            if (item is Video video)
+            {
+                items = video.GetAdditionalParts()
+                    .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+                    .ToArray();
+            }
+            else
+            {
+                items = Array.Empty<BaseItemDto>();
+            }
+
+            var result = new QueryResult<BaseItemDto>
+            {
+                Items = items,
+                TotalRecordCount = items.Length
+            };
+
+            return result;
+        }
+
+        /// <summary>
+        /// Removes alternate video sources.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <response code="204">Alternate sources deleted.</response>
+        /// <response code="404">Video not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
+        [HttpDelete("{itemId}/AlternateSources")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult DeleteAlternateSources([FromRoute] Guid itemId)
+        {
+            var video = (Video)_libraryManager.GetItemById(itemId);
+
+            if (video == null)
+            {
+                return NotFound("The video either does not exist or the id does not belong to a video.");
+            }
+
+            foreach (var link in video.GetLinkedAlternateVersions())
+            {
+                link.SetPrimaryVersionId(null);
+                link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+
+                link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+            }
+
+            video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+            video.SetPrimaryVersionId(null);
+            video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Merges videos into a single record.
+        /// </summary>
+        /// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param>
+        /// <response code="204">Videos merged.</response>
+        /// <response code="400">Supply at least 2 video ids.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
+        [HttpPost("MergeVersions")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status400BadRequest)]
+        public ActionResult MergeVersions([FromQuery] string itemIds)
+        {
+            var items = RequestHelpers.Split(itemIds, ',', true)
+                .Select(i => _libraryManager.GetItemById(i))
+                .OfType<Video>()
+                .OrderBy(i => i.Id)
+                .ToList();
+
+            if (items.Count < 2)
+            {
+                return BadRequest("Please supply at least two videos to merge.");
+            }
+
+            var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
+
+            var primaryVersion = videosWithVersions.FirstOrDefault();
+            if (primaryVersion == null)
+            {
+                primaryVersion = items
+                    .OrderBy(i =>
+                    {
+                        if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
+                        {
+                            return 1;
+                        }
+
+                        return 0;
+                    })
+                    .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
+                    .First();
+            }
+
+            var list = primaryVersion.LinkedAlternateVersions.ToList();
+
+            foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
+            {
+                item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
+
+                item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+
+                list.Add(new LinkedChild
+                {
+                    Path = item.Path,
+                    ItemId = item.Id
+                });
+
+                foreach (var linkedItem in item.LinkedAlternateVersions)
+                {
+                    if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
+                    {
+                        list.Add(linkedItem);
+                    }
+                }
+
+                if (item.LinkedAlternateVersions.Length > 0)
+                {
+                    item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+                    item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+                }
+            }
+
+            primaryVersion.LinkedAlternateVersions = list.ToArray();
+            primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+            return NoContent();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs
deleted file mode 100644
index 957a279f85..0000000000
--- a/MediaBrowser.Api/VideosService.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Videos/{Id}/AdditionalParts", "GET", Summary = "Gets additional parts for a video.")]
-    [Authenticated]
-    public class GetAdditionalParts : IReturn<QueryResult<BaseItemDto>>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Videos/{Id}/AlternateSources", "DELETE", Summary = "Removes alternate video sources.")]
-    [Authenticated(Roles = "Admin")]
-    public class DeleteAlternateSources : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Videos/MergeVersions", "POST", Summary = "Merges videos into a single record")]
-    [Authenticated(Roles = "Admin")]
-    public class MergeVersions : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id list. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
-    }
-
-    public class VideosService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserManager _userManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        public VideosService(
-            ILogger<VideosService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            IUserManager userManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetAdditionalParts request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id)
-                           ? (!request.UserId.Equals(Guid.Empty)
-                                  ? _libraryManager.GetUserRootFolder()
-                                  : _libraryManager.RootFolder)
-                           : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            BaseItemDto[] items;
-            if (item is Video video)
-            {
-                items = video.GetAdditionalParts()
-                    .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
-                    .ToArray();
-            }
-            else
-            {
-                items = Array.Empty<BaseItemDto>();
-            }
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = items,
-                TotalRecordCount = items.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Delete(DeleteAlternateSources request)
-        {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
-
-            foreach (var link in video.GetLinkedAlternateVersions())
-            {
-                link.SetPrimaryVersionId(null);
-                link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
-
-                link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-            }
-
-            video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
-            video.SetPrimaryVersionId(null);
-            video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-        }
-
-        public void Post(MergeVersions request)
-        {
-            var items = request.Ids.Split(',')
-                .Select(i => _libraryManager.GetItemById(i))
-                .OfType<Video>()
-                .OrderBy(i => i.Id)
-                .ToList();
-
-            if (items.Count < 2)
-            {
-                throw new ArgumentException("Please supply at least two videos to merge.");
-            }
-
-            var videosWithVersions = items.Where(i => i.MediaSourceCount > 1)
-                .ToList();
-
-            var primaryVersion = videosWithVersions.FirstOrDefault();
-            if (primaryVersion == null)
-            {
-                primaryVersion = items.OrderBy(i =>
-                    {
-                        if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile)
-                        {
-                            return 1;
-                        }
-
-                        return 0;
-                    })
-                    .ThenByDescending(i =>
-                    {
-                        return i.GetDefaultVideoStream()?.Width ?? 0;
-                    }).First();
-            }
-
-            var list = primaryVersion.LinkedAlternateVersions.ToList();
-
-            foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
-            {
-                item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
-
-                item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-
-                list.Add(new LinkedChild
-                {
-                    Path = item.Path,
-                    ItemId = item.Id
-                });
-
-                foreach (var linkedItem in item.LinkedAlternateVersions)
-                {
-                    if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
-                    {
-                        list.Add(linkedItem);
-                    }
-                }
-
-                if (item.LinkedAlternateVersions.Length > 0)
-                {
-                    item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
-                    item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-                }
-            }
-
-            primaryVersion.LinkedAlternateVersions = list.ToArray();
-            primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-        }
-    }
-}

From c492c09b2568af5bf179f617dc192012969a8602 Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 09:33:34 -0600
Subject: [PATCH 234/463] Update
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 56ac215a96..846cd849a3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="client">Client.</param>
         /// <param name="displayPreferences">New Display Preferences object.</param>
         /// <response code="204">Display preferences updated.</response>
-        /// <returns>An <see cref="OkResult"/> on success.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
         [HttpPost("{displayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]

From 857eb0c397e742be0813d2bbed4231cbd356d556 Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 09:33:40 -0600
Subject: [PATCH 235/463] Update Jellyfin.Api/Controllers/PluginsController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/PluginsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 8a0913307e..ecae630111 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="pluginId">Plugin id.</param>
         /// <response code="204">Plugin uninstalled.</response>
         /// <response code="404">Plugin not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
         [HttpDelete("{pluginId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]

From f6972cdab89efeb7792f740f97f6685ad904f82b Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 09:33:46 -0600
Subject: [PATCH 236/463] Update
 Jellyfin.Api/Controllers/ItemRefreshController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/ItemRefreshController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index f10f9fb3d8..e527e54107 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
         /// <response code="204">Item metadata refresh queued.</response>
         /// <response code="404">Item to refresh not found.</response>
-        /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+        /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
         [HttpPost("{itemId}/Refresh")]
         [Description("Refreshes metadata for an item.")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]

From f211a6c17b9171b2d1191b3a35ac8f34490ff0cc Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 09:33:52 -0600
Subject: [PATCH 237/463] Update Jellyfin.Api/Controllers/PluginsController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/PluginsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index ecae630111..f6036b748d 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -107,7 +107,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Plugin not found or plugin does not have configuration.</response>
         /// <returns>
         /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
-        ///    The task result contains an <see cref="OkResult"/> indicating success, or <see cref="NotFoundResult"/>
+        ///    The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
         ///    when plugin not found or plugin doesn't have configuration.
         /// </returns>
         [HttpPost("{pluginId}/Configuration")]

From cffba00f51ef57ba2334b4c30431c028b2ecbe1a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 09:35:38 -0600
Subject: [PATCH 238/463] Fix response code

---
 Jellyfin.Api/Controllers/ItemLookupController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index e474f2b23d..75cba450f9 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -286,10 +286,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <param name="searchResult">The remote search result.</param>
         /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
-        /// <response code="200">Item metadata refreshed.</response>
+        /// <response code="204">Item metadata refreshed.</response>
         /// <returns>
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
-        /// The task result contains an <see cref="OkResult"/>.
+        /// The task result contains an <see cref="NoContentResult"/>.
         /// </returns>
         [HttpPost("/Items/RemoteSearch/Apply/{id}")]
         [Authorize(Policy = Policies.RequiresElevation)]
@@ -318,7 +318,7 @@ namespace Jellyfin.Api.Controllers
                     SearchResult = searchResult
                 }, CancellationToken.None).ConfigureAwait(false);
 
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -361,4 +361,4 @@ namespace Jellyfin.Api.Controllers
         private string GetFullCachePath(string filename)
             => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
     }
-}
\ No newline at end of file
+}

From fae510308e9b3e5022e8d9b67b07f1a5cda026b0 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 21 Jun 2020 18:00:04 +0200
Subject: [PATCH 239/463] Add try catch block

---
 .../Controllers/DashboardController.cs        | 21 +++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 21c320a490..6f162aacca 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -22,6 +23,7 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     public class DashboardController : BaseJellyfinApiController
     {
+        private readonly ILogger<DashboardController> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IConfiguration _appConfig;
         private readonly IServerConfigurationManager _serverConfigurationManager;
@@ -30,16 +32,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardController"/> class.
         /// </summary>
+        /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
         /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
         /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
         public DashboardController(
+            ILogger<DashboardController> logger,
             IServerApplicationHost appHost,
             IConfiguration appConfig,
             IResourceFileManager resourceFileManager,
             IServerConfigurationManager serverConfigurationManager)
         {
+            _logger = logger;
             _appHost = appHost;
             _appConfig = appConfig;
             _resourceFileManager = resourceFileManager;
@@ -63,7 +68,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/web/ConfigurationPages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
+        public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
             [FromQuery] bool? enableInMainMenu,
             [FromQuery] ConfigurationPageType? pageType)
         {
@@ -79,7 +84,15 @@ namespace Jellyfin.Api.Controllers
             // Don't allow a failing plugin to fail them all
             var configPages = pages.Select(p =>
                 {
-                    return new ConfigurationPageInfo(p);
+                    try
+                    {
+                        return new ConfigurationPageInfo(p);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
+                        return null;
+                    }
                 })
                 .Where(i => i != null)
                 .ToList();
@@ -88,12 +101,12 @@ namespace Jellyfin.Api.Controllers
 
             if (pageType.HasValue)
             {
-                configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList();
+                configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList();
             }
 
             if (enableInMainMenu.HasValue)
             {
-                configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
+                configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
             }
 
             return configPages;

From 4eb94b8fb18ef2fdddfb7a7fe1cde484e6c6ff06 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 21 Jun 2020 18:22:17 +0200
Subject: [PATCH 240/463] Update
 Jellyfin.Api/Controllers/DashboardController.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/DashboardController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 6f162aacca..aab920ff36 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -259,12 +259,12 @@ namespace Jellyfin.Api.Controllers
 
         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
         {
-            if (!(plugin is IHasWebPages))
+            if (!(plugin is IHasWebPages hasWebPages))
             {
                 return new List<Tuple<PluginPageInfo, IPlugin>>();
             }
 
-            return (plugin as IHasWebPages)!.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
+            return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
         }
 
         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()

From ccd7b3f52435de880158bc41dec9268dc9acbdd5 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 11:31:44 -0600
Subject: [PATCH 241/463] WIP GetImage endpoints

---
 Jellyfin.Api/Controllers/ImageController.cs | 401 +++++++++++++++++++-
 1 file changed, 397 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index d8c67dbea0..c24c5e24c5 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -21,6 +23,7 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -302,16 +305,172 @@ namespace Jellyfin.Api.Controllers
             return list;
         }
 
+        /// <summary>
+        /// Gets the item's image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Items/{itemId}/Images/{imageType}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}")]
+        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        public async Task<ActionResult> GetItemImage(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] string tag,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] string format,
+            [FromQuery] bool addPlayedIndicator,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? blur,
+            [FromQuery] string backgroundColor,
+            [FromQuery] string foregroundLayer,
+            [FromRoute] int? imageIndex = null,
+            [FromQuery] bool enableImageEnhancers = true)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    itemId,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    enableImageEnhancers,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the item's image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        public ActionResult<object> GetItemImage(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? imageIndex,
+            [FromRoute] string tag,
+            [FromRoute] string format,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string backgroundColor,
+            [FromQuery] string foregroundLayer,
+            [FromQuery] bool enableImageEnhancers = true)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return GetImageInternal(
+                itemId,
+                imageType,
+                imageIndex,
+                tag,
+                format,
+                maxWidth,
+                maxHeight,
+                percentPlayed,
+                unplayedCount,
+                width,
+                height,
+                quality,
+                cropWhitespace,
+                addPlayedIndicator,
+                blur,
+                backgroundColor,
+                foregroundLayer,
+                enableImageEnhancers,
+                item,
+                Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase));
+        }
+
         private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
         {
             using var reader = new StreamReader(inputStream);
             var text = await reader.ReadToEndAsync().ConfigureAwait(false);
 
             var bytes = Convert.FromBase64String(text);
-            return new MemoryStream(bytes)
-            {
-                Position = 0
-            };
+            return new MemoryStream(bytes) {Position = 0};
         }
 
         private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
@@ -365,5 +524,239 @@ namespace Jellyfin.Api.Controllers
                 return null;
             }
         }
+
+        private async Task<ActionResult> GetImageInternal(
+            Guid itemId,
+            ImageType imageType,
+            int? imageIndex,
+            string tag,
+            string format,
+            int? maxWidth,
+            int? maxHeight,
+            double? percentPlayed,
+            int? unplayedCount,
+            int? width,
+            int? height,
+            int? quality,
+            bool? cropWhitespace,
+            bool addPlayedIndicator,
+            int? blur,
+            string backgroundColor,
+            string foregroundLayer,
+            bool enableImageEnhancers,
+            BaseItem item,
+            bool isHeadRequest)
+        {
+            if (percentPlayed.HasValue)
+            {
+                if (percentPlayed.Value <= 0)
+                {
+                    percentPlayed = null;
+                }
+                else if (percentPlayed.Value >= 100)
+                {
+                    percentPlayed = null;
+                    addPlayedIndicator = true;
+                }
+            }
+
+            if (percentPlayed.HasValue)
+            {
+                unplayedCount = null;
+            }
+
+            if (unplayedCount.HasValue
+                && unplayedCount.Value <= 0)
+            {
+                unplayedCount = null;
+            }
+
+            var imageInfo = item.GetImageInfo(imageType, imageIndex ?? 0);
+            if (imageInfo == null)
+            {
+                return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item.Name, imageType));
+            }
+
+            if (!cropWhitespace.HasValue)
+            {
+                cropWhitespace = imageType == ImageType.Logo || imageType == ImageType.Art;
+            }
+
+            var outputFormats = GetOutputFormats(format);
+
+            TimeSpan? cacheDuration = null;
+
+            if (!string.IsNullOrEmpty(tag))
+            {
+                cacheDuration = TimeSpan.FromDays(365);
+            }
+
+            var responseHeaders = new Dictionary<string, string> {{"transferMode.dlna.org", "Interactive"}, {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}};
+
+            return await GetImageResult(
+                item,
+                itemId,
+                imageIndex,
+                height,
+                maxHeight,
+                maxWidth,
+                quality,
+                width,
+                addPlayedIndicator,
+                percentPlayed,
+                unplayedCount,
+                blur,
+                backgroundColor,
+                foregroundLayer,
+                imageInfo,
+                cropWhitespace.Value,
+                outputFormats,
+                cacheDuration,
+                responseHeaders,
+                isHeadRequest).ConfigureAwait(false);
+        }
+
+        private ImageFormat[] GetOutputFormats(string format)
+        {
+            if (!string.IsNullOrWhiteSpace(format)
+                && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+            {
+                return new[] {parsedFormat};
+            }
+
+            return GetClientSupportedFormats();
+        }
+
+        private ImageFormat[] GetClientSupportedFormats()
+        {
+            var acceptTypes = Request.Headers[HeaderNames.Accept];
+            var supportedFormats = new List<string>();
+            if (acceptTypes.Count > 0)
+            {
+                foreach (var type in acceptTypes)
+                {
+                    int index = type.IndexOf(';', StringComparison.Ordinal);
+                    if (index != -1)
+                    {
+                        supportedFormats.Add(type.Substring(0, index));
+                    }
+                }
+            }
+
+            var acceptParam = Request.Query[HeaderNames.Accept];
+
+            var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+
+            if (!supportsWebP)
+            {
+                var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
+                if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
+                    userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    supportsWebP = true;
+                }
+            }
+
+            var formats = new List<ImageFormat>(4);
+
+            if (supportsWebP)
+            {
+                formats.Add(ImageFormat.Webp);
+            }
+
+            formats.Add(ImageFormat.Jpg);
+            formats.Add(ImageFormat.Png);
+
+            if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+            {
+                formats.Add(ImageFormat.Gif);
+            }
+
+            return formats.ToArray();
+        }
+
+        private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+        {
+            var mimeType = "image/" + format;
+
+            if (requestAcceptTypes.Contains(mimeType))
+            {
+                return true;
+            }
+
+            if (acceptAll && requestAcceptTypes.Contains("*/*"))
+            {
+                return true;
+            }
+
+            return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+        }
+
+        private async Task<ActionResult> GetImageResult(
+            BaseItem item,
+            Guid itemId,
+            int? index,
+            int? height,
+            int? maxHeight,
+            int? maxWidth,
+            int? quality,
+            int? width,
+            bool addPlayedIndicator,
+            double? percentPlayed,
+            int? unplayedCount,
+            int? blur,
+            string backgroundColor,
+            string foregroundLayer,
+            ItemImageInfo imageInfo,
+            bool cropWhitespace,
+            IReadOnlyCollection<ImageFormat> supportedFormats,
+            TimeSpan? cacheDuration,
+            IDictionary<string, string> headers,
+            bool isHeadRequest)
+        {
+            if (!imageInfo.IsLocalFile)
+            {
+                imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false);
+            }
+
+            var options = new ImageProcessingOptions
+            {
+                CropWhiteSpace = cropWhitespace,
+                Height = height,
+                ImageIndex = index ?? 0,
+                Image = imageInfo,
+                Item = item,
+                ItemId = itemId,
+                MaxHeight = maxHeight,
+                MaxWidth = maxWidth,
+                Quality = quality ?? 100,
+                Width = width,
+                AddPlayedIndicator = addPlayedIndicator,
+                PercentPlayed = percentPlayed ?? 0,
+                UnplayedCount = unplayedCount,
+                Blur = blur,
+                BackgroundColor = backgroundColor,
+                ForegroundLayer = foregroundLayer,
+                SupportedOutputFormats = supportedFormats
+            };
+
+            var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
+
+            headers[HeaderNames.Vary] = HeaderNames.Accept;
+            /*
+             // TODO
+            return _resultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            {
+                CacheDuration = cacheDuration,
+                ResponseHeaders = headers,
+                ContentType = imageResult.Item2,
+                DateLastModified = imageResult.Item3,
+                IsHeadRequest = isHeadRequest,
+                Path = imageResult.Item1,
+                FileShare = FileShare.Read
+            });
+            */
+            return NoContent();
+        }
     }
 }

From f35774170fedd24697604923bd0d516b5dad9cd2 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 21 Jun 2020 16:12:21 -0600
Subject: [PATCH 242/463] Move LiveTvService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/LiveTvController.cs  | 1151 +++++++++++++++++
 Jellyfin.Api/Extensions/DtoExtensions.cs      |    6 +-
 Jellyfin.Api/Helpers/RequestHelpers.cs        |   78 +-
 .../LiveTvDtos/ChannelMappingOptionsDto.cs    |   32 +
 .../Models/LiveTvDtos/GetProgramsDto.cs       |  166 +++
 MediaBrowser.Api/LiveTv/LiveTvService.cs      |  792 ------------
 6 files changed, 1429 insertions(+), 796 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/LiveTvController.cs
 create mode 100644 Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
 create mode 100644 Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs

diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
new file mode 100644
index 0000000000..1279d42997
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -0,0 +1,1151 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Mime;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.LiveTvDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Live tv controller.
+    /// </summary>
+    public class LiveTvController : BaseJellyfinApiController
+    {
+        private readonly ILiveTvManager _liveTvManager;
+        private readonly IUserManager _userManager;
+        private readonly IHttpClient _httpClient;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ISessionContext _sessionContext;
+        private readonly IStreamHelper _streamHelper;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LiveTvController"/> class.
+        /// </summary>
+        /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        public LiveTvController(
+            ILiveTvManager liveTvManager,
+            IUserManager userManager,
+            IHttpClient httpClient,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            IAuthorizationContext authContext,
+            ISessionContext sessionContext,
+            IStreamHelper streamHelper,
+            IMediaSourceManager mediaSourceManager,
+            IConfigurationManager configurationManager)
+        {
+            _liveTvManager = liveTvManager;
+            _userManager = userManager;
+            _httpClient = httpClient;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _authContext = authContext;
+            _sessionContext = sessionContext;
+            _streamHelper = streamHelper;
+            _mediaSourceManager = mediaSourceManager;
+            _configurationManager = configurationManager;
+        }
+
+        /// <summary>
+        /// Gets available live tv services.
+        /// </summary>
+        /// <response code="200">Available live tv services returned.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the available live tv services.
+        /// </returns>
+        [HttpGet("Info")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<LiveTvInfo> GetLiveTvInfo()
+        {
+            return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Gets available live tv channels.
+        /// </summary>
+        /// <param name="type">Optional. Filter by channel type.</param>
+        /// <param name="userId">Optional. Filter by user and attach user data.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="isMovie">Optional. Filter for movies.</param>
+        /// <param name="isSeries">Optional. Filter for series.</param>
+        /// <param name="isNews">Optional. Filter for news.</param>
+        /// <param name="isKids">Optional. Filter for kids.</param>
+        /// <param name="isSports">Optional. Filter for sports.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
+        /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
+        /// <param name="isDisliked">Optional. Filter by channels that are disliked, or not.</param>
+        /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">"Optional. The image types to include in the output.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="sortBy">Optional. Key to sort by.</param>
+        /// <param name="sortOrder">Optional. Sort order.</param>
+        /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
+        /// <response code="200">Available live tv channels returned.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the resulting available live tv channels.
+        /// </returns>
+        [HttpGet("Channels")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+            [FromQuery] ChannelType? type,
+            [FromQuery] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isSports,
+            [FromQuery] int? limit,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] bool? isLiked,
+            [FromQuery] bool? isDisliked,
+            [FromQuery] bool enableFavoriteSorting,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] string sortBy,
+            [FromQuery] SortOrder? sortOrder,
+            [FromQuery] bool addCurrentProgram = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            var channelResult = _liveTvManager.GetInternalChannels(
+                new LiveTvChannelQuery
+                {
+                    ChannelType = type,
+                    UserId = userId,
+                    StartIndex = startIndex,
+                    Limit = limit,
+                    IsFavorite = isFavorite,
+                    IsLiked = isLiked,
+                    IsDisliked = isDisliked,
+                    EnableFavoriteSorting = enableFavoriteSorting,
+                    IsMovie = isMovie,
+                    IsSeries = isSeries,
+                    IsNews = isNews,
+                    IsKids = isKids,
+                    IsSports = isSports,
+                    SortBy = RequestHelpers.Split(sortBy, ',', true),
+                    SortOrder = sortOrder ?? SortOrder.Ascending,
+                    AddCurrentProgram = addCurrentProgram
+                },
+                dtoOptions,
+                CancellationToken.None);
+
+            var user = userId.Equals(Guid.Empty)
+                ? null
+                : _userManager.GetUserById(userId);
+
+            var fieldsList = dtoOptions.Fields.ToList();
+            fieldsList.Remove(ItemFields.CanDelete);
+            fieldsList.Remove(ItemFields.CanDownload);
+            fieldsList.Remove(ItemFields.DisplayPreferencesId);
+            fieldsList.Remove(ItemFields.Etag);
+            dtoOptions.Fields = fieldsList.ToArray();
+            dtoOptions.AddCurrentProgram = addCurrentProgram;
+
+            var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user);
+            return new QueryResult<BaseItemDto>
+            {
+                Items = returnArray,
+                TotalRecordCount = channelResult.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets a live tv channel.
+        /// </summary>
+        /// <param name="channelId">Channel id.</param>
+        /// <param name="userId">Optional. Attach user data.</param>
+        /// <response code="200">Live tv channel returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
+        [HttpGet("Channels/{channelId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<BaseItemDto> GetChannel([FromRoute] Guid channelId, [FromQuery] Guid userId)
+        {
+            var user = _userManager.GetUserById(userId);
+            var item = channelId.Equals(Guid.Empty)
+                ? _libraryManager.GetUserRootFolder()
+                : _libraryManager.GetItemById(channelId);
+
+            var dtoOptions = new DtoOptions()
+                .AddClientFields(Request);
+            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+        }
+
+        /// <summary>
+        /// Gets live tv recordings.
+        /// </summary>
+        /// <param name="channelId">Optional. Filter by channel id.</param>
+        /// <param name="userId">Optional. Filter by user and attach user data.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="status">Optional. Filter by recording status.</param>
+        /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+        /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="isMovie">Optional. Filter for movies.</param>
+        /// <param name="isSeries">Optional. Filter for series.</param>
+        /// <param name="isKids">Optional. Filter for kids.</param>
+        /// <param name="isSports">Optional. Filter for sports.</param>
+        /// <param name="isNews">Optional. Filter for news.</param>
+        /// <param name="isLibraryItem">Optional. Filter for is library item.</param>
+        /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+        /// <response code="200">Live tv recordings returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+        [HttpGet("Recordings")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
+            [FromQuery] string channelId,
+            [FromQuery] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] RecordingStatus? status,
+            [FromQuery] bool? isInProgress,
+            [FromQuery] string seriesTimerId,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isSports,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isLibraryItem,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            return _liveTvManager.GetRecordings(
+                new RecordingQuery
+            {
+                ChannelId = channelId,
+                UserId = userId,
+                StartIndex = startIndex,
+                Limit = limit,
+                Status = status,
+                SeriesTimerId = seriesTimerId,
+                IsInProgress = isInProgress,
+                EnableTotalRecordCount = enableTotalRecordCount,
+                IsMovie = isMovie,
+                IsNews = isNews,
+                IsSeries = isSeries,
+                IsKids = isKids,
+                IsSports = isSports,
+                IsLibraryItem = isLibraryItem,
+                Fields = RequestHelpers.GetItemFields(fields),
+                ImageTypeLimit = imageTypeLimit,
+                EnableImages = enableImages
+            }, dtoOptions);
+        }
+
+        /// <summary>
+        /// Gets live tv recording series.
+        /// </summary>
+        /// <param name="channelId">Optional. Filter by channel id.</param>
+        /// <param name="userId">Optional. Filter by user and attach user data.</param>
+        /// <param name="groupId">Optional. Filter by recording group.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="status">Optional. Filter by recording status.</param>
+        /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+        /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+        /// <response code="200">Live tv recordings returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+        [HttpGet("Recordings/Series")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Obsolete("This endpoint is obsolete.")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
+            [FromQuery] string channelId,
+            [FromQuery] Guid userId,
+            [FromQuery] string groupId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] RecordingStatus? status,
+            [FromQuery] bool? isInProgress,
+            [FromQuery] string seriesTimerId,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            return new QueryResult<BaseItemDto>();
+        }
+
+        /// <summary>
+        /// Gets live tv recording groups.
+        /// </summary>
+        /// <param name="userId">Optional. Filter by user and attach user data.</param>
+        /// <response code="200">Recording groups returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
+        [HttpGet("Recordings/Groups")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Obsolete("This endpoint is obsolete.")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid userId)
+        {
+            return new QueryResult<BaseItemDto>();
+        }
+
+        /// <summary>
+        /// Gets recording folders.
+        /// </summary>
+        /// <param name="userId">Optional. Filter by user and attach user data.</param>
+        /// <response code="200">Recording folders returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
+        [HttpGet("Recordings/Folders")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid userId)
+        {
+            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+            var folders = _liveTvManager.GetRecordingFolders(user);
+
+            var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = returnArray,
+                TotalRecordCount = returnArray.Count
+            };
+        }
+
+        /// <summary>
+        /// Gets a live tv recording.
+        /// </summary>
+        /// <param name="recordingId">Recording id.</param>
+        /// <param name="userId">Optional. Attach user data.</param>
+        /// <response code="200">Recording returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
+        [HttpGet("Recordings/{recordingId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult<BaseItemDto> GetRecording([FromRoute] Guid recordingId, [FromQuery] Guid userId)
+        {
+            var user = _userManager.GetUserById(userId);
+            var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+
+            var dtoOptions = new DtoOptions()
+                .AddClientFields(Request);
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+        }
+
+        /// <summary>
+        /// Resets a tv tuner.
+        /// </summary>
+        /// <param name="tunerId">Tuner id.</param>
+        /// <response code="204">Tuner reset.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("Tuners/{tunerId}/Reset")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public ActionResult ResetTuner([FromRoute] string tunerId)
+        {
+            AssertUserCanManageLiveTv();
+            _liveTvManager.ResetTuner(tunerId, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets a timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <response code="200">Timer returned.</response>
+        /// <returns>
+        /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the timer.
+        /// </returns>
+        [HttpGet("Timers/{timerId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public async Task<ActionResult<TimerInfoDto>> GetTimer(string timerId)
+        {
+            return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the default values for a new timer.
+        /// </summary>
+        /// <param name="programId">Optional. To attach default values based on a program.</param>
+        /// <response code="200">Default values returned.</response>
+        /// <returns>
+        /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the default values for a timer.
+        /// </returns>
+        [HttpGet("Timers/Defaults")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string programId)
+        {
+            return string.IsNullOrEmpty(programId)
+                ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
+                : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the live tv timers.
+        /// </summary>
+        /// <param name="channelId">Optional. Filter by channel id.</param>
+        /// <param name="seriesTimerId">Optional. Filter by timers belonging to a series timer.</param>
+        /// <param name="isActive">Optional. Filter by timers that are active.</param>
+        /// <param name="isScheduled">Optional. Filter by timers that are scheduled.</param>
+        /// <returns>
+        /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the live tv timers.
+        /// </returns>
+        [HttpGet("Timers")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
+            [FromQuery] string channelId,
+            [FromQuery] string seriesTimerId,
+            [FromQuery] bool? isActive,
+            [FromQuery] bool? isScheduled)
+        {
+            return await _liveTvManager.GetTimers(
+                    new TimerQuery
+                    {
+                        ChannelId = channelId,
+                        SeriesTimerId = seriesTimerId,
+                        IsActive = isActive,
+                        IsScheduled = isScheduled
+                    }, CancellationToken.None)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets available live tv epgs.
+        /// </summary>
+        /// <param name="channelIds">The channels to return guide information for.</param>
+        /// <param name="userId">Optional. Filter by user id.</param>
+        /// <param name="minStartDate">Optional. The minimum premiere start date.</param>
+        /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
+        /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
+        /// <param name="maxStartDate">Optional. The maximum premiere start date.</param>
+        /// <param name="minEndDate">Optional. The minimum premiere end date.</param>
+        /// <param name="maxEndDate">Optional. The maximum premiere end date.</param>
+        /// <param name="isMovie">Optional. Filter for movies.</param>
+        /// <param name="isSeries">Optional. Filter for series.</param>
+        /// <param name="isNews">Optional. Filter for news.</param>
+        /// <param name="isKids">Optional. Filter for kids.</param>
+        /// <param name="isSports">Optional. Filter for sports.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="genres">The genres to return guide information for.</param>
+        /// <param name="genreIds">The genre ids to return guide information for.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
+        /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+        /// <response code="200">Live tv epgs returned.</response>
+        /// <returns>
+        /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+        /// </returns>
+        [HttpGet("Programs")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms(
+            [FromQuery] string channelIds,
+            [FromQuery] Guid userId,
+            [FromQuery] DateTime? minStartDate,
+            [FromQuery] bool? hasAired,
+            [FromQuery] bool? isAiring,
+            [FromQuery] DateTime? maxStartDate,
+            [FromQuery] DateTime? minEndDate,
+            [FromQuery] DateTime? maxEndDate,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isSports,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string sortBy,
+            [FromQuery] string sortOrder,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] string seriesTimerId,
+            [FromQuery] Guid librarySeriesId,
+            [FromQuery] string fields,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ChannelIds = RequestHelpers.Split(channelIds, ',', true)
+                    .Select(i => new Guid(i)).ToArray(),
+                HasAired = hasAired,
+                IsAiring = isAiring,
+                EnableTotalRecordCount = enableTotalRecordCount,
+                MinStartDate = minStartDate,
+                MinEndDate = minEndDate,
+                MaxStartDate = maxStartDate,
+                MaxEndDate = maxEndDate,
+                StartIndex = startIndex,
+                Limit = limit,
+                OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+                IsNews = isNews,
+                IsMovie = isMovie,
+                IsSeries = isSeries,
+                IsKids = isKids,
+                IsSports = isSports,
+                SeriesTimerId = seriesTimerId,
+                Genres = RequestHelpers.Split(genres, ',', true),
+                GenreIds = RequestHelpers.GetGuids(genreIds)
+            };
+
+            if (!librarySeriesId.Equals(Guid.Empty))
+            {
+                query.IsSeries = true;
+
+                if (_libraryManager.GetItemById(librarySeriesId) is Series series)
+                {
+                    query.Name = series.Name;
+                }
+            }
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+            return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets available live tv epgs.
+        /// </summary>
+        /// <param name="body">Request body.</param>
+        /// <response code="200">Live tv epgs returned.</response>
+        /// <returns>
+        /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+        /// </returns>
+        [HttpPost("Programs")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+        {
+            var user = body.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(body.UserId);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
+                    .Select(i => new Guid(i)).ToArray(),
+                HasAired = body.HasAired,
+                IsAiring = body.IsAiring,
+                EnableTotalRecordCount = body.EnableTotalRecordCount,
+                MinStartDate = body.MinStartDate,
+                MinEndDate = body.MinEndDate,
+                MaxStartDate = body.MaxStartDate,
+                MaxEndDate = body.MaxEndDate,
+                StartIndex = body.StartIndex,
+                Limit = body.Limit,
+                OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
+                IsNews = body.IsNews,
+                IsMovie = body.IsMovie,
+                IsSeries = body.IsSeries,
+                IsKids = body.IsKids,
+                IsSports = body.IsSports,
+                SeriesTimerId = body.SeriesTimerId,
+                Genres = RequestHelpers.Split(body.Genres, ',', true),
+                GenreIds = RequestHelpers.GetGuids(body.GenreIds)
+            };
+
+            if (!body.LibrarySeriesId.Equals(Guid.Empty))
+            {
+                query.IsSeries = true;
+
+                if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
+                {
+                    query.Name = series.Name;
+                }
+            }
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(body.Fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
+            return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets recommended live tv epgs.
+        /// </summary>
+        /// <param name="userId">Optional. filter by user id.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
+        /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
+        /// <param name="isSeries">Optional. Filter for series.</param>
+        /// <param name="isMovie">Optional. Filter for movies.</param>
+        /// <param name="isNews">Optional. Filter for news.</param>
+        /// <param name="isKids">Optional. Filter for kids.</param>
+        /// <param name="isSports">Optional. Filter for sports.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="genreIds">The genres to return guide information for.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="enableUserData">Optional. include user data.</param>
+        /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+        /// <response code="200">Recommended epgs returned.</response>
+        /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
+        [HttpGet("Programs/Recommended")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetRecommendedPrograms(
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] bool? isAiring,
+            [FromQuery] bool? hasAired,
+            [FromQuery] bool? isSeries,
+            [FromQuery] bool? isMovie,
+            [FromQuery] bool? isNews,
+            [FromQuery] bool? isKids,
+            [FromQuery] bool? isSports,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string genreIds,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var query = new InternalItemsQuery(user)
+            {
+                IsAiring = isAiring,
+                Limit = limit,
+                HasAired = hasAired,
+                IsSeries = isSeries,
+                IsMovie = isMovie,
+                IsKids = isKids,
+                IsNews = isNews,
+                IsSports = isSports,
+                EnableTotalRecordCount = enableTotalRecordCount,
+                GenreIds = RequestHelpers.GetGuids(genreIds)
+            };
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+            return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Deletes a live tv recording.
+        /// </summary>
+        /// <param name="recordingId">Recording id.</param>
+        /// <response code="204">Recording deleted.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpDelete("Recordings/{recordingId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult DeleteRecording([FromRoute] Guid recordingId)
+        {
+            AssertUserCanManageLiveTv();
+
+            var item = _libraryManager.GetItemById(recordingId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            _libraryManager.DeleteItem(item, new DeleteOptions
+            {
+                DeleteFileLocation = false
+            });
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Cancels a live tv timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <response code="204">Timer deleted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("Timers/{timerId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> CancelTimer([FromRoute] string timerId)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates a live tv timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <param name="timerInfo">New timer info.</param>
+        /// <response code="204">Timer updated.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("Timers/{timerId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Creates a live tv timer.
+        /// </summary>
+        /// <param name="timerInfo">New timer info.</param>
+        /// <response code="204">Timer created.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("Timers")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets a live tv series timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <response code="200">Series timer returned.</response>
+        /// <response code="404">Series timer not found.</response>
+        /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
+        [HttpGet("SeriesTimers/{timerId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute] string timerId)
+        {
+            var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+            if (timer == null)
+            {
+                return NotFound();
+            }
+
+            return timer;
+        }
+
+        /// <summary>
+        /// Gets live tv series timers.
+        /// </summary>
+        /// <param name="sortBy">Optional. Sort by SortName or Priority.</param>
+        /// <param name="sortOrder">Optional. Sort in Ascending or Descending order.</param>
+        /// <response code="200">Timers returned.</response>
+        /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
+        [HttpGet("SeriesTimers")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string sortBy, [FromQuery] SortOrder sortOrder)
+        {
+            return await _liveTvManager.GetSeriesTimers(
+                new SeriesTimerQuery
+                {
+                    SortOrder = sortOrder,
+                    SortBy = sortBy
+                }, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Cancels a live tv series timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <response code="204">Timer cancelled.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("SeriesTimers/{timerId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> CancelSeriesTimer([FromRoute] string timerId)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates a live tv series timer.
+        /// </summary>
+        /// <param name="timerId">Timer id.</param>
+        /// <param name="seriesTimerInfo">New series timer info.</param>
+        /// <response code="204">Series timer updated.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("SeriesTimers/{timerId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> UpdateSeriesTimer([FromRoute] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Creates a live tv series timer.
+        /// </summary>
+        /// <param name="seriesTimerInfo">New series timer info.</param>
+        /// <response code="204">Series timer info created.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("SeriesTimers")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
+        {
+            AssertUserCanManageLiveTv();
+            await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Get recording group.
+        /// </summary>
+        /// <param name="groupId">Group id.</param>
+        /// <returns>A <see cref="NotFoundResult"/>.</returns>
+        [HttpGet("Recordings/Groups/{groupId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [Obsolete("This endpoint is obsolete.")]
+        public ActionResult<BaseItemDto> GetRecordingGroup([FromQuery] Guid groupId)
+        {
+            return NotFound();
+        }
+
+        /// <summary>
+        /// Get guid info.
+        /// </summary>
+        /// <response code="200">Guid info returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
+        [HttpGet("GuideInfo")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<GuideInfo> GetGuideInfo()
+        {
+            return _liveTvManager.GetGuideInfo();
+        }
+
+        /// <summary>
+        /// Adds a tuner host.
+        /// </summary>
+        /// <param name="tunerHostInfo">New tuner host.</param>
+        /// <response code="200">Created tuner host returned.</response>
+        /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
+        [HttpPost("TunerHosts")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
+        {
+            return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Deletes a tuner host.
+        /// </summary>
+        /// <param name="id">Tuner host id.</param>
+        /// <response code="204">Tuner host deleted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("TunerHosts")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult DeleteTunerHost([FromQuery] string id)
+        {
+            var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+            config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
+            _configurationManager.SaveConfiguration("livetv", config);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets default listings provider info.
+        /// </summary>
+        /// <response code="200">Default listings provider info returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
+        [HttpGet("ListingProviders/Default")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
+        {
+            return new ListingsProviderInfo();
+        }
+
+        /// <summary>
+        /// Adds a listings provider.
+        /// </summary>
+        /// <param name="validateLogin">Validate login.</param>
+        /// <param name="validateListings">Validate listings.</param>
+        /// <param name="pw">Password.</param>
+        /// <param name="listingsProviderInfo">New listings info.</param>
+        /// <response code="200">Created listings provider returned.</response>
+        /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
+        [HttpGet("ListingProviders")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
+            [FromQuery] bool validateLogin,
+            [FromQuery] bool validateListings,
+            [FromQuery] string pw,
+            [FromBody] ListingsProviderInfo listingsProviderInfo)
+        {
+            using var sha = SHA1.Create();
+            if (!string.IsNullOrEmpty(pw))
+            {
+                listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
+            }
+
+            return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Delete listing provider.
+        /// </summary>
+        /// <param name="id">Listing provider id.</param>
+        /// <response code="204">Listing provider deleted.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpGet("ListingProviders")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult DeleteListingProvider([FromQuery] string id)
+        {
+            _liveTvManager.DeleteListingsProvider(id);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets available lineups.
+        /// </summary>
+        /// <param name="id">Provider id.</param>
+        /// <param name="type">Provider type.</param>
+        /// <param name="location">Location.</param>
+        /// <param name="country">Country.</param>
+        /// <response code="200">Available lineups returned.</response>
+        /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
+        [HttpGet("ListingProviders/Lineups")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
+            [FromQuery] string id,
+            [FromQuery] string type,
+            [FromQuery] string location,
+            [FromQuery] string country)
+        {
+            return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets available countries.
+        /// </summary>
+        /// <response code="200">Available countries returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
+        [HttpGet("ListingProviders/SchedulesDirect/Countries")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetSchedulesDirectCountries()
+        {
+            // https://json.schedulesdirect.org/20141201/available/countries
+            var response = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = "https://json.schedulesdirect.org/20141201/available/countries",
+                BufferContent = false
+            }).ConfigureAwait(false);
+            return File(response, MediaTypeNames.Application.Json);
+        }
+
+        /// <summary>
+        /// Get channel mapping options.
+        /// </summary>
+        /// <param name="providerId">Provider id.</param>
+        /// <response code="200">Channel mapping options returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
+        [HttpGet("ChannelMappingOptions")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string providerId)
+        {
+            var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+
+            var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
+
+            var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
+
+            var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
+                .ConfigureAwait(false);
+
+            var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
+                .ConfigureAwait(false);
+
+            var mappings = listingsProviderInfo.ChannelMappings;
+
+            return new ChannelMappingOptionsDto
+            {
+                TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
+                ProviderChannels = providerChannels.Select(i => new NameIdPair
+                {
+                    Name = i.Name,
+                    Id = i.Id
+                }).ToList(),
+                Mappings = mappings,
+                ProviderName = listingsProviderName
+            };
+        }
+
+        /// <summary>
+        /// Set channel mappings.
+        /// </summary>
+        /// <param name="providerId">Provider id.</param>
+        /// <param name="tunerChannelId">Tuner channel id.</param>
+        /// <param name="providerChannelId">Provider channel id.</param>
+        /// <response code="200">Created channel mapping returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
+        [HttpPost("ChannelMappings")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
+            [FromQuery] string providerId,
+            [FromQuery] string tunerChannelId,
+            [FromQuery] string providerChannelId)
+        {
+            return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get tuner host types.
+        /// </summary>
+        /// <response code="200">Tuner host types returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
+        [HttpGet("TunerHosts/Types")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
+        {
+            return _liveTvManager.GetTunerHostTypes();
+        }
+
+        /// <summary>
+        /// Discover tuners.
+        /// </summary>
+        /// <param name="newDevicesOnly">Only discover new tuners.</param>
+        /// <response code="200">Tuners returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
+        [HttpGet("Tuners/Discvover")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly)
+        {
+            return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
+        }
+
+        private void AssertUserCanManageLiveTv()
+        {
+            var user = _sessionContext.GetUser(Request);
+
+            if (user == null)
+            {
+                throw new SecurityException("Anonymous live tv management is not allowed.");
+            }
+
+            if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
+            {
+                throw new SecurityException("The current user does not have permission to manage live tv.");
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 4c587391fc..e61e9c29d9 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Extensions
         /// <param name="dtoOptions">DtoOptions object.</param>
         /// <param name="fields">Comma delimited string of fields.</param>
         /// <returns>Modified DtoOptions object.</returns>
-        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields)
+        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields)
         {
             if (string.IsNullOrEmpty(fields))
             {
@@ -122,11 +122,11 @@ namespace Jellyfin.Api.Extensions
         /// <param name="enableImageTypes">Enable image types.</param>
         /// <returns>Modified DtoOptions object.</returns>
         internal static DtoOptions AddAdditionalDtoOptions(
-            in DtoOptions dtoOptions,
+            this DtoOptions dtoOptions,
             bool? enableImages,
             bool? enableUserData,
             int? imageTypeLimit,
-            string enableImageTypes)
+            string? enableImageTypes)
         {
             dtoOptions.EnableImages = enableImages ?? true;
 
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 2ff40a8a5e..ce29b90c6a 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,7 +1,10 @@
 using System;
+using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
@@ -18,7 +21,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="separator">The char that separates the substrings.</param>
         /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
         /// <returns>An array of the substrings.</returns>
-        internal static string[] Split(string value, char separator, bool removeEmpty)
+        internal static string[] Split(string? value, char separator, bool removeEmpty)
         {
             if (string.IsNullOrWhiteSpace(value))
             {
@@ -73,5 +76,78 @@ namespace Jellyfin.Api.Helpers
 
             return session;
         }
+
+        /// <summary>
+        /// Gets the item fields.
+        /// </summary>
+        /// <param name="fields">The item field string.</param>
+        /// <returns>Array of parsed item fields.</returns>
+        internal static ItemFields[] GetItemFields(string fields)
+        {
+            if (string.IsNullOrEmpty(fields))
+            {
+                return Array.Empty<ItemFields>();
+            }
+
+            return Split(fields, ',', true)
+                .Select(v =>
+                {
+                    if (Enum.TryParse(v, true, out ItemFields value))
+                    {
+                        return (ItemFields?)value;
+                    }
+
+                    return null;
+                })
+                .Where(i => i.HasValue)
+                .Select(i => i!.Value)
+                .ToArray();
+        }
+
+        internal static Guid[] GetGuids(string? value)
+        {
+            if (string.IsNullOrEmpty(value))
+            {
+                return Array.Empty<Guid>();
+            }
+
+            return Split(value, ',', true)
+                .Select(i => new Guid(i))
+                .ToArray();
+        }
+
+        internal static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
+        {
+            var val = sortBy;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return Array.Empty<ValueTuple<string, SortOrder>>();
+            }
+
+            var vals = val.Split(',');
+            if (string.IsNullOrWhiteSpace(requestedSortOrder))
+            {
+                requestedSortOrder = "Ascending";
+            }
+
+            var sortOrders = requestedSortOrder.Split(',');
+
+            var result = new ValueTuple<string, SortOrder>[vals.Length];
+
+            for (var i = 0; i < vals.Length; i++)
+            {
+                var sortOrderIndex = sortOrders.Length > i ? i : 0;
+
+                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
+                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
+                    ? SortOrder.Descending
+                    : SortOrder.Ascending;
+
+                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
+            }
+
+            return result;
+        }
     }
 }
diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
new file mode 100644
index 0000000000..642b40e4d2
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+    /// <summary>
+    /// Channel mapping options dto.
+    /// </summary>
+    public class ChannelMappingOptionsDto
+    {
+        /// <summary>
+        /// Gets or sets list of tuner channels.
+        /// </summary>
+        public List<TunerChannelMapping> TunerChannels { get; set; }
+
+        /// <summary>
+        /// Gets or sets list of provider channels.
+        /// </summary>
+        public List<NameIdPair> ProviderChannels { get; set; }
+
+        /// <summary>
+        /// Gets or sets list of mappings.
+        /// </summary>
+        public NameValuePair[] Mappings { get; set; }
+
+        /// <summary>
+        /// Gets or sets provider name.
+        /// </summary>
+        public string? ProviderName { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
new file mode 100644
index 0000000000..d7eaab30de
--- /dev/null
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -0,0 +1,166 @@
+using System;
+
+namespace Jellyfin.Api.Models.LiveTvDtos
+{
+    /// <summary>
+    /// Get programs dto.
+    /// </summary>
+    public class GetProgramsDto
+    {
+        /// <summary>
+        /// Gets or sets the channels to return guide information for.
+        /// </summary>
+        public string? ChannelIds { get; set; }
+
+        /// <summary>
+        /// Gets or sets optional. Filter by user id.
+        /// </summary>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the minimum premiere start date.
+        /// Optional.
+        /// </summary>
+        public DateTime? MinStartDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter by programs that have completed airing, or not.
+        /// Optional.
+        /// </summary>
+        public bool? HasAired { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter by programs that are currently airing, or not.
+        /// Optional.
+        /// </summary>
+        public bool? IsAiring { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum premiere start date.
+        /// Optional.
+        /// </summary>
+        public DateTime? MaxStartDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the minimum premiere end date.
+        /// Optional.
+        /// </summary>
+        public DateTime? MinEndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum premiere end date.
+        /// Optional.
+        /// </summary>
+        public DateTime? MaxEndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter for movies.
+        /// Optional.
+        /// </summary>
+        public bool? IsMovie { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter for series.
+        /// Optional.
+        /// </summary>
+        public bool? IsSeries { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter for news.
+        /// Optional.
+        /// </summary>
+        public bool? IsNews { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter for kids.
+        /// Optional.
+        /// </summary>
+        public bool? IsKids { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter for sports.
+        /// Optional.
+        /// </summary>
+        public bool? IsSports { get; set; }
+
+        /// <summary>
+        /// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
+        /// Optional.
+        /// </summary>
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets the maximum number of records to return.
+        /// Optional.
+        /// </summary>
+        public int? Limit { get; set; }
+
+        /// <summary>
+        /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
+        /// Optional.
+        /// </summary>
+        public string? SortBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets sort Order - Ascending,Descending.
+        /// </summary>
+        public string? SortOrder { get; set; }
+
+        /// <summary>
+        /// Gets or sets the genres to return guide information for.
+        /// </summary>
+        public string? Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets the genre ids to return guide information for.
+        /// </summary>
+        public string? GenreIds { get; set; }
+
+        /// <summary>
+        /// Gets or sets include image information in output.
+        /// Optional.
+        /// </summary>
+        public bool? EnableImages { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether retrieve total record count.
+        /// </summary>
+        public bool EnableTotalRecordCount { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets the max number of images to return, per image type.
+        /// Optional.
+        /// </summary>
+        public int? ImageTypeLimit { get; set; }
+
+        /// <summary>
+        /// Gets or sets the image types to include in the output.
+        /// Optional.
+        /// </summary>
+        public string? EnableImageTypes { get; set; }
+
+        /// <summary>
+        /// Gets or sets include user data.
+        /// Optional.
+        /// </summary>
+        public bool? EnableUserData { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter by series timer id.
+        /// Optional.
+        /// </summary>
+        public string? SeriesTimerId { get; set; }
+
+        /// <summary>
+        /// Gets or sets filter by library series id.
+        /// Optional.
+        /// </summary>
+        public Guid LibrarySeriesId { get; set; }
+
+        /// <summary>
+        /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
+        /// Optional.
+        /// </summary>
+        public string? Fields { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs
index 279fd6ee9b..14abdcc998 100644
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs
@@ -30,638 +30,6 @@ using Microsoft.Net.Http.Headers;
 
 namespace MediaBrowser.Api.LiveTv
 {
-    /// <summary>
-    /// This is insecure right now to avoid windows phone refactoring
-    /// </summary>
-    [Route("/LiveTv/Info", "GET", Summary = "Gets available live tv services.")]
-    [Authenticated]
-    public class GetLiveTvInfo : IReturn<LiveTvInfo>
-    {
-    }
-
-    [Route("/LiveTv/Channels", "GET", Summary = "Gets available live tv channels.")]
-    [Authenticated]
-    public class GetChannels : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public ChannelType? Type { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "IsFavorite", Description = "Filter by channels that are favorites, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-
-        [ApiMember(Name = "IsLiked", Description = "Filter by channels that are liked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsLiked { get; set; }
-
-        [ApiMember(Name = "IsDisliked", Description = "Filter by channels that are disliked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsDisliked { get; set; }
-
-        [ApiMember(Name = "EnableFavoriteSorting", Description = "Incorporate favorite and like status into channel sorting.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool EnableFavoriteSorting { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool AddCurrentProgram { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public string SortBy { get; set; }
-
-        public SortOrder? SortOrder { get; set; }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public string[] GetOrderBy()
-        {
-            var val = SortBy;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return Array.Empty<string>();
-            }
-
-            return val.Split(',');
-        }
-
-        public GetChannels()
-        {
-            AddCurrentProgram = true;
-        }
-    }
-
-    [Route("/LiveTv/Channels/{Id}", "GET", Summary = "Gets a live tv channel")]
-    [Authenticated]
-    public class GetChannel : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings", "GET", Summary = "Gets live tv recordings")]
-    [Authenticated]
-    public class GetRecordings : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public RecordingStatus? Status { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsInProgress { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public bool? IsMovie { get; set; }
-        public bool? IsSeries { get; set; }
-        public bool? IsKids { get; set; }
-        public bool? IsSports { get; set; }
-        public bool? IsNews { get; set; }
-        public bool? IsLibraryItem { get; set; }
-
-        public GetRecordings()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Recordings/Series", "GET", Summary = "Gets live tv recordings")]
-    [Authenticated]
-    public class GetRecordingSeries : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "GroupId", Description = "Optional filter by recording group.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string GroupId { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public RecordingStatus? Status { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsInProgress { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public GetRecordingSeries()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Recordings/Groups", "GET", Summary = "Gets live tv recording groups")]
-    [Authenticated]
-    public class GetRecordingGroups : IReturn<QueryResult<BaseItemDto>>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings/Folders", "GET", Summary = "Gets recording folders")]
-    [Authenticated]
-    public class GetRecordingFolders : IReturn<BaseItemDto[]>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings/{Id}", "GET", Summary = "Gets a live tv recording")]
-    [Authenticated]
-    public class GetRecording : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Tuners/{Id}/Reset", "POST", Summary = "Resets a tv tuner")]
-    [Authenticated]
-    public class ResetTuner : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Tuner Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "GET", Summary = "Gets a live tv timer")]
-    [Authenticated]
-    public class GetTimer : IReturn<TimerInfoDto>
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/Defaults", "GET", Summary = "Gets default values for a new timer")]
-    [Authenticated]
-    public class GetDefaultTimer : IReturn<SeriesTimerInfoDto>
-    {
-        [ApiMember(Name = "ProgramId", Description = "Optional, to attach default values based on a program.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProgramId { get; set; }
-    }
-
-    [Route("/LiveTv/Timers", "GET", Summary = "Gets live tv timers")]
-    [Authenticated]
-    public class GetTimers : IReturn<QueryResult<TimerInfoDto>>
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by timers belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        public bool? IsActive { get; set; }
-
-        public bool? IsScheduled { get; set; }
-    }
-
-    [Route("/LiveTv/Programs", "GET,POST", Summary = "Gets available live tv epgs..")]
-    [Authenticated]
-    public class GetPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelIds", Description = "The channels to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ChannelIds { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "MinStartDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MinStartDate { get; set; }
-
-        [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasAired { get; set; }
-        public bool? IsAiring { get; set; }
-
-        [ApiMember(Name = "MaxStartDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MaxStartDate { get; set; }
-
-        [ApiMember(Name = "MinEndDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MinEndDate { get; set; }
-
-        [ApiMember(Name = "MaxEndDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MaxEndDate { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Name, StartDate", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        [ApiMember(Name = "Genres", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string Genres { get; set; }
-
-        [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string GenreIds { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public string SeriesTimerId { get; set; }
-        public Guid LibrarySeriesId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public GetPrograms()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")]
-    [Authenticated]
-    public class GetRecommendedPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        public bool EnableTotalRecordCount { get; set; }
-
-        public GetRecommendedPrograms()
-        {
-            EnableTotalRecordCount = true;
-        }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "IsAiring", Description = "Optional. Filter by programs that are currently airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsAiring { get; set; }
-
-        [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasAired { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string GenreIds { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
-    [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")]
-    [Authenticated]
-    public class GetProgram : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-
-    [Route("/LiveTv/Recordings/{Id}", "DELETE", Summary = "Deletes a live tv recording")]
-    [Authenticated]
-    public class DeleteRecording : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "DELETE", Summary = "Cancels a live tv timer")]
-    [Authenticated]
-    public class CancelTimer : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "POST", Summary = "Updates a live tv timer")]
-    [Authenticated]
-    public class UpdateTimer : TimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/Timers", "POST", Summary = "Creates a live tv timer")]
-    [Authenticated]
-    public class CreateTimer : TimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "GET", Summary = "Gets a live tv series timer")]
-    [Authenticated]
-    public class GetSeriesTimer : IReturn<TimerInfoDto>
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers", "GET", Summary = "Gets live tv series timers")]
-    [Authenticated]
-    public class GetSeriesTimers : IReturn<QueryResult<SeriesTimerInfoDto>>
-    {
-        [ApiMember(Name = "SortBy", Description = "Optional. Sort by SortName or Priority", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Optional. Sort in Ascending or Descending order", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public SortOrder SortOrder { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "DELETE", Summary = "Cancels a live tv series timer")]
-    [Authenticated]
-    public class CancelSeriesTimer : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "POST", Summary = "Updates a live tv series timer")]
-    [Authenticated]
-    public class UpdateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/SeriesTimers", "POST", Summary = "Creates a live tv series timer")]
-    [Authenticated]
-    public class CreateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/Recordings/Groups/{Id}", "GET", Summary = "Gets a recording group")]
-    [Authenticated]
-    public class GetRecordingGroup : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Recording group Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/GuideInfo", "GET", Summary = "Gets guide info")]
-    [Authenticated]
-    public class GetGuideInfo : IReturn<GuideInfo>
-    {
-    }
-
-    [Route("/LiveTv/TunerHosts", "POST", Summary = "Adds a tuner host")]
-    [Authenticated]
-    public class AddTunerHost : TunerHostInfo, IReturn<TunerHostInfo>
-    {
-    }
-
-    [Route("/LiveTv/TunerHosts", "DELETE", Summary = "Deletes a tuner host")]
-    [Authenticated]
-    public class DeleteTunerHost : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Tuner host id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/Default", "GET")]
-    [Authenticated]
-    public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
-    {
-    }
-
-    [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
-    [Authenticated]
-    public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
-    {
-        public bool ValidateLogin { get; set; }
-        public bool ValidateListings { get; set; }
-        public string Pw { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
-    [Authenticated]
-    public class DeleteListingProvider : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
-    [Authenticated]
-    public class GetLineups : IReturn<List<NameIdPair>>
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Type", Description = "Provider Type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Type { get; set; }
-
-        [ApiMember(Name = "Location", Description = "Location", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Location { get; set; }
-
-        [ApiMember(Name = "Country", Description = "Country", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Country { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")]
-    [Authenticated]
-    public class GetSchedulesDirectCountries
-    {
-    }
-
-    [Route("/LiveTv/ChannelMappingOptions")]
-    [Authenticated]
-    public class GetChannelMappingOptions
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
-        public string ProviderId { get; set; }
-    }
-
-    [Route("/LiveTv/ChannelMappings")]
-    [Authenticated]
-    public class SetChannelMapping
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
-        public string ProviderId { get; set; }
-        public string TunerChannelId { get; set; }
-        public string ProviderChannelId { get; set; }
-    }
-
-    public class ChannelMappingOptions
-    {
-        public List<TunerChannelMapping> TunerChannels { get; set; }
-        public List<NameIdPair> ProviderChannels { get; set; }
-        public NameValuePair[] Mappings { get; set; }
-        public string ProviderName { get; set; }
-    }
-
     [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
     public class GetLiveStreamFile
     {
@@ -675,20 +43,6 @@ namespace MediaBrowser.Api.LiveTv
         public string Id { get; set; }
     }
 
-    [Route("/LiveTv/TunerHosts/Types", "GET")]
-    [Authenticated]
-    public class GetTunerHostTypes : IReturn<List<NameIdPair>>
-    {
-
-    }
-
-    [Route("/LiveTv/Tuners/Discvover", "GET")]
-    [Authenticated]
-    public class DiscoverTuners : IReturn<List<TunerHostInfo>>
-    {
-        public bool NewDevicesOnly { get; set; }
-    }
-
     public class LiveTvService : BaseApiService
     {
         private readonly ILiveTvManager _liveTvManager;
@@ -733,22 +87,6 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedResult(list);
         }
 
-        public object Get(GetRecordingFolders request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-            var folders = _liveTvManager.GetRecordingFolders(user);
-
-            var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnArray,
-                TotalRecordCount = returnArray.Count
-            };
-
-            return ToOptimizedResult(result);
-        }
-
         public object Get(GetLiveRecordingFile request)
         {
             var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
@@ -929,55 +267,6 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedResult(info);
         }
 
-        public object Get(GetLiveTvInfo request)
-        {
-            var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None);
-
-            return ToOptimizedResult(info);
-        }
-
-        public object Get(GetChannels request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var channelResult = _liveTvManager.GetInternalChannels(new LiveTvChannelQuery
-            {
-                ChannelType = request.Type,
-                UserId = request.UserId,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                IsFavorite = request.IsFavorite,
-                IsLiked = request.IsLiked,
-                IsDisliked = request.IsDisliked,
-                EnableFavoriteSorting = request.EnableFavoriteSorting,
-                IsMovie = request.IsMovie,
-                IsSeries = request.IsSeries,
-                IsNews = request.IsNews,
-                IsKids = request.IsKids,
-                IsSports = request.IsSports,
-                SortBy = request.GetOrderBy(),
-                SortOrder = request.SortOrder ?? SortOrder.Ascending,
-                AddCurrentProgram = request.AddCurrentProgram
-
-            }, options, CancellationToken.None);
-
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            RemoveFields(options);
-
-            options.AddCurrentProgram = request.AddCurrentProgram;
-
-            var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnArray,
-                TotalRecordCount = channelResult.TotalRecordCount
-            };
-
-            return ToOptimizedResult(result);
-        }
-
         private void RemoveFields(DtoOptions options)
         {
             var fields = options.Fields.ToList();
@@ -989,19 +278,6 @@ namespace MediaBrowser.Api.LiveTv
             options.Fields = fields.ToArray();
         }
 
-        public object Get(GetChannel request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
         public async Task<object> Get(GetPrograms request)
         {
             var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
@@ -1090,74 +366,6 @@ namespace MediaBrowser.Api.LiveTv
             return Get(request);
         }
 
-        public object Get(GetRecordings request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var result = _liveTvManager.GetRecordings(new RecordingQuery
-            {
-                ChannelId = request.ChannelId,
-                UserId = request.UserId,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                Status = request.Status,
-                SeriesTimerId = request.SeriesTimerId,
-                IsInProgress = request.IsInProgress,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                IsMovie = request.IsMovie,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries,
-                IsKids = request.IsKids,
-                IsSports = request.IsSports,
-                IsLibraryItem = request.IsLibraryItem,
-                Fields = request.GetItemFields(),
-                ImageTypeLimit = request.ImageTypeLimit,
-                EnableImages = request.EnableImages
-
-            }, options);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetRecordingSeries request)
-        {
-            return ToOptimizedResult(new QueryResult<BaseItemDto>());
-        }
-
-        public object Get(GetRecording request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetTimer request)
-        {
-            var result = await _liveTvManager.GetTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetTimers request)
-        {
-            var result = await _liveTvManager.GetTimers(new TimerQuery
-            {
-                ChannelId = request.ChannelId,
-                SeriesTimerId = request.SeriesTimerId,
-                IsActive = request.IsActive,
-                IsScheduled = request.IsScheduled
-
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
         public void Delete(DeleteRecording request)
         {
             AssertUserCanManageLiveTv();

From a50738e88d3e91bf1c2be02cc0cda89768387990 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 22 Jun 2020 07:37:29 -0600
Subject: [PATCH 243/463] move BrandingService.cs to Jellyfin.Api

---
 .../Controllers/BrandingController.cs         | 62 +++++++++++++++++++
 .../Properties/launchSettings.json            |  3 +-
 2 files changed, 64 insertions(+), 1 deletion(-)
 create mode 100644 Jellyfin.Api/Controllers/BrandingController.cs

diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
new file mode 100644
index 0000000000..d580fedffd
--- /dev/null
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Branding;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Branding controller.
+    /// </summary>
+    public class BrandingController : BaseJellyfinApiController
+    {
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BrandingController"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public BrandingController(IServerConfigurationManager serverConfigurationManager)
+        {
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Gets branding configuration.
+        /// </summary>
+        /// <response code="200">Branding configuration returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
+        [HttpGet("Configuration")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BrandingOptions> GetBrandingOptions()
+        {
+            return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+        }
+
+        /// <summary>
+        /// Gets branding css.
+        /// </summary>
+        /// <response code="200">Branding css returned.</response>
+        /// <response code="204">No branding css configured.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the branding css if exist,
+        /// or a <see cref="NoContentResult"/> if the css is not configured.
+        /// </returns>
+        [HttpGet("Css")]
+        [HttpGet("Css.css")]
+        [Produces("text/css")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult<string> GetBrandingCss()
+        {
+            var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+            if (string.IsNullOrEmpty(options.CustomCss))
+            {
+                return NoContent();
+            }
+
+            return options.CustomCss;
+        }
+    }
+}
diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json
index b6e2bcf976..a716387091 100644
--- a/Jellyfin.Server/Properties/launchSettings.json
+++ b/Jellyfin.Server/Properties/launchSettings.json
@@ -4,7 +4,8 @@
       "commandName": "Project",
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
-      }
+      },
+        "commandLineArgs": "--webdir C:\\Users\\Cody\\Code\\Jellyfin\\tmp\\jf-web-wizard\\dist"
     },
     "Jellyfin.Server (nowebclient)": {
       "commandName": "Project",

From 5c6e9f4db58883db43055cd37b2cecd9fa2c12b2 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 22 Jun 2020 15:44:11 +0200
Subject: [PATCH 244/463] Add missing authorization policies

---
 .../Controllers/DisplayPreferencesController.cs      |  3 ++-
 Jellyfin.Api/Controllers/FilterController.cs         |  3 ++-
 Jellyfin.Api/Controllers/ImageByNameController.cs    |  7 ++++---
 Jellyfin.Api/Controllers/ItemLookupController.cs     |  2 +-
 Jellyfin.Api/Controllers/ItemRefreshController.cs    |  3 ++-
 Jellyfin.Api/Controllers/PlaylistsController.cs      |  3 ++-
 Jellyfin.Api/Controllers/PluginsController.cs        |  2 +-
 Jellyfin.Api/Controllers/RemoteImageController.cs    |  3 ++-
 Jellyfin.Api/Controllers/SessionController.cs        |  3 ++-
 Jellyfin.Api/Controllers/UserController.cs           | 12 ++++++------
 Jellyfin.Api/Controllers/VideosController.cs         |  2 +-
 11 files changed, 25 insertions(+), 18 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 846cd849a3..3f946d9d22 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using System.Diagnostics.CodeAnalysis;
 using System.Threading;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
@@ -13,7 +14,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Display Preferences Controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DisplayPreferencesController : BaseJellyfinApiController
     {
         private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index dc5b0d9061..0934a116a0 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -18,7 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Filters controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class FilterController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 0e3c32d3cc..4800c0608f 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Net.Mime;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Retrieved list of images.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("General")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
         {
@@ -88,7 +89,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Retrieved list of images.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("Ratings")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
         {
@@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Image list retrieved.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
         [HttpGet("MediaInfo")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
         {
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 75cba450f9..44709d0ee6 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Item lookup controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ItemLookupController : BaseJellyfinApiController
     {
         private readonly IProviderManager _providerManager;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index e527e54107..e6cdf4edbb 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
@@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     /// [Authenticated]
     [Route("/Items")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ItemRefreshController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 2e3f6c54af..2dc0d2dc71 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaylistDtos;
@@ -20,7 +21,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Playlists controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PlaylistsController : BaseJellyfinApiController
     {
         private readonly IPlaylistManager _playlistManager;
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index f6036b748d..979d401191 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Plugins controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PluginsController : BaseJellyfinApiController
     {
         private readonly IApplicationHost _appHost;
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 41b7f98ee1..a0d14be7a5 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -5,6 +5,7 @@ using System.Linq;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers
     /// Remote Images Controller.
     /// </summary>
     [Route("Images")]
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class RemoteImageController : BaseJellyfinApiController
     {
         private readonly IProviderManager _providerManager;
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 315bc9728b..39da4178d6 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Devices;
@@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">List of sessions returned.</response>
         /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
         [HttpGet("/Sessions")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<SessionInfo>> GetSessions(
             [FromQuery] Guid controllableByUserId,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 0d57dcc837..c1f417df52 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Users returned.</response>
         /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
         [HttpGet]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<UserDto>> GetUsers(
@@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
         [HttpPost("{userId}/Password")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -295,7 +295,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">User not found.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
         [HttpPost("{userId}/EasyPassword")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -337,7 +337,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
         [HttpPost("{userId}")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
@@ -381,7 +381,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User policy update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
         [HttpPost("{userId}/Policy")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
@@ -437,7 +437,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User configuration update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{userId}/Configuration")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         public ActionResult UpdateUserConfiguration(
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 532ce59c50..effe630a9b 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Additional parts returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
         [HttpGet("{itemId}/AdditionalParts")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId)
         {

From 263d925e4fb5faa56f33120f2b09f6254c3ddc92 Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Mon, 22 Jun 2020 07:46:47 -0600
Subject: [PATCH 245/463] Update launchSettings.json

---
 Jellyfin.Server/Properties/launchSettings.json | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json
index a716387091..b6e2bcf976 100644
--- a/Jellyfin.Server/Properties/launchSettings.json
+++ b/Jellyfin.Server/Properties/launchSettings.json
@@ -4,8 +4,7 @@
       "commandName": "Project",
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
-      },
-        "commandLineArgs": "--webdir C:\\Users\\Cody\\Code\\Jellyfin\\tmp\\jf-web-wizard\\dist"
+      }
     },
     "Jellyfin.Server (nowebclient)": {
       "commandName": "Project",

From 0fa316c9e4c1156b1ed4a37b4ade204aa4f8a392 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 22 Jun 2020 07:47:13 -0600
Subject: [PATCH 246/463] move BrandingService.cs to Jellyfin.Api

---
 MediaBrowser.Api/BrandingService.cs | 44 -----------------------------
 1 file changed, 44 deletions(-)
 delete mode 100644 MediaBrowser.Api/BrandingService.cs

diff --git a/MediaBrowser.Api/BrandingService.cs b/MediaBrowser.Api/BrandingService.cs
deleted file mode 100644
index f4724e7745..0000000000
--- a/MediaBrowser.Api/BrandingService.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Branding;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Branding/Configuration", "GET", Summary = "Gets branding configuration")]
-    public class GetBrandingOptions : IReturn<BrandingOptions>
-    {
-    }
-
-    [Route("/Branding/Css", "GET", Summary = "Gets custom css")]
-    [Route("/Branding/Css.css", "GET", Summary = "Gets custom css")]
-    public class GetBrandingCss
-    {
-    }
-
-    public class BrandingService : BaseApiService
-    {
-        public BrandingService(
-            ILogger<BrandingService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-        }
-
-        public object Get(GetBrandingOptions request)
-        {
-            return ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding");
-        }
-
-        public object Get(GetBrandingCss request)
-        {
-            var result = ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding");
-
-            // When null this throws a 405 error under Mono OSX, so default to empty string
-            return ResultFactory.GetResult(Request, result.CustomCss ?? string.Empty, "text/css");
-        }
-    }
-}

From 6b72fb86316786451be4fb84e4ba89d496b4ef2f Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 22 Jun 2020 15:49:15 +0200
Subject: [PATCH 247/463] Add missing default authorization policy

---
 Jellyfin.Api/Controllers/SystemController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index f4dae40ef6..e33821b248 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
         [HttpGet("Endpoint")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<EndPointInfo> GetEndpointInfo()
         {
@@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
         [HttpGet("WakeOnLanInfo")]
-        [Authorize]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
         {

From 1d7d480efe52589557bdc6371731fad6d15ee1f6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 22 Jun 2020 08:14:07 -0600
Subject: [PATCH 248/463] fix tests

---
 .../Controllers/BrandingController.cs         |  7 +----
 MediaBrowser.Api/TestService.cs               | 26 +++++++++++++++++++
 tests/Jellyfin.Api.Tests/GetPathValueTests.cs |  4 +--
 .../BrandingServiceTests.cs                   |  2 +-
 4 files changed, 30 insertions(+), 9 deletions(-)
 create mode 100644 MediaBrowser.Api/TestService.cs

diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index d580fedffd..67790c0e4a 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -51,12 +51,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<string> GetBrandingCss()
         {
             var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
-            if (string.IsNullOrEmpty(options.CustomCss))
-            {
-                return NoContent();
-            }
-
-            return options.CustomCss;
+            return options.CustomCss ?? string.Empty;
         }
     }
 }
diff --git a/MediaBrowser.Api/TestService.cs b/MediaBrowser.Api/TestService.cs
new file mode 100644
index 0000000000..6c999e08d1
--- /dev/null
+++ b/MediaBrowser.Api/TestService.cs
@@ -0,0 +1,26 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Service for testing path value.
+    /// </summary>
+    public class TestService : BaseApiService
+    {
+        /// <summary>
+        /// Test service.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param>
+        public TestService(
+            ILogger<TestService> logger,
+            IServerConfigurationManager serverConfigurationManager,
+            IHttpResultFactory httpResultFactory)
+            : base(logger, serverConfigurationManager, httpResultFactory)
+        {
+        }
+    }
+}
diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
index b01d1af1f0..397eb2edc3 100644
--- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
+++ b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
@@ -31,8 +31,8 @@ namespace Jellyfin.Api.Tests
 
             var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf);
 
-            var service = new BrandingService(
-                new NullLogger<BrandingService>(),
+            var service = new TestService(
+                new NullLogger<TestService>(),
                 confManagerMock,
                 Mock.Of<IHttpResultFactory>())
             {
diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
index 34698fe251..5d7f7765cd 100644
--- a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
+++ b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
@@ -43,7 +43,7 @@ namespace MediaBrowser.Api.Tests
 
             // Assert
             response.EnsureSuccessStatusCode();
-            Assert.Equal("text/css", response.Content.Headers.ContentType.ToString());
+            Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType.ToString());
         }
     }
 }

From 293d96f27c42d929f50b4e947fe988555708963a Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 22 Jun 2020 18:02:57 +0200
Subject: [PATCH 249/463] Move TvShowsService to Jellyfin.Api started

---
 Jellyfin.Api/Controllers/TvShowsController.cs | 172 ++++++++++++++++
 MediaBrowser.Api/TvShowsService.cs            | 189 ------------------
 2 files changed, 172 insertions(+), 189 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/TvShowsController.cs

diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
new file mode 100644
index 0000000000..ba3c9fd66a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.TV;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The tv shows controller.
+    /// </summary>
+    [Route("/Shows")]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class TvShowsController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly ITVSeriesManager _tvSeriesManager;
+        private readonly IAuthorizationContext _authContext;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TvShowsController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        public TvShowsController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            ITVSeriesManager tvSeriesManager,
+            IAuthorizationContext authContext)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _tvSeriesManager = tvSeriesManager;
+            _authContext = authContext;
+        }
+
+        /// <summary>
+        /// Gets a list of next up episodes.
+        /// </summary>
+        /// <param name="userId">The user id of the user to get the next up episodes for.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="seriesId">Optional. Filter by series id.</param>
+        /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="enableImges">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+        [HttpGet("NextUp")]
+        public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
+            [FromQuery] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string? fields,
+            [FromQuery] string? seriesId,
+            [FromQuery] string? parentId,
+            [FromQuery] bool? enableImges,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var options = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+
+            var result = _tvSeriesManager.GetNextUp(
+                new NextUpQuery
+                {
+                    Limit = limit,
+                    ParentId = parentId,
+                    SeriesId = seriesId,
+                    StartIndex = startIndex,
+                    UserId = userId,
+                    EnableTotalRecordCount = enableTotalRecordCount
+                },
+                options);
+
+            var user = _userManager.GetUserById(userId);
+
+            var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = result.TotalRecordCount,
+                Items = returnItems
+            };
+        }
+
+        /// <summary>
+        /// Gets a list of upcoming episodes.
+        /// </summary>
+        /// <param name="userId">The user id of the user to get the upcoming episodes for.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="seriesId">Optional. Filter by series id.</param>
+        /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="enableImges">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+        [HttpGet("Upcoming")]
+        public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
+            [FromQuery] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string? fields,
+            [FromQuery] string? seriesId,
+            [FromQuery] string? parentId,
+            [FromQuery] bool? enableImges,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+
+            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+
+            var options = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+
+            var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
+            {
+                IncludeItemTypes = new[] { nameof(Episode) },
+                OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+                MinPremiereDate = minPremiereDate,
+                StartIndex = startIndex,
+                Limit = limit,
+                ParentId = parentIdGuid,
+                Recursive = true,
+                DtoOptions = options
+            });
+
+            var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = itemsResult.Count,
+                Items = returnItems
+            };
+        }
+    }
+}
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
index 0c23d8b291..ab2a2ddaa0 100644
--- a/MediaBrowser.Api/TvShowsService.cs
+++ b/MediaBrowser.Api/TvShowsService.cs
@@ -18,120 +18,6 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api
 {
-    /// <summary>
-    /// Class GetNextUpEpisodes
-    /// </summary>
-    [Route("/Shows/NextUp", "GET", Summary = "Gets a list of next up episodes")]
-    public class GetNextUpEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "SeriesId", Description = "Optional. Filter by series id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesId { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-        public bool EnableTotalRecordCount { get; set; }
-
-        public GetNextUpEpisodes()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
-    public class GetUpcomingEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
     [Route("/Shows/{Id}/Episodes", "GET", Summary = "Gets episodes for a tv season")]
     public class GetEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
     {
@@ -248,18 +134,9 @@ namespace MediaBrowser.Api
     [Authenticated]
     public class TvShowsService : BaseApiService
     {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
         private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager
-        /// </summary>
         private readonly ILibraryManager _libraryManager;
-
         private readonly IDtoService _dtoService;
-        private readonly ITVSeriesManager _tvSeriesManager;
         private readonly IAuthorizationContext _authContext;
 
         /// <summary>
@@ -275,81 +152,15 @@ namespace MediaBrowser.Api
             IUserManager userManager,
             ILibraryManager libraryManager,
             IDtoService dtoService,
-            ITVSeriesManager tvSeriesManager,
             IAuthorizationContext authContext)
             : base(logger, serverConfigurationManager, httpResultFactory)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
             _dtoService = dtoService;
-            _tvSeriesManager = tvSeriesManager;
             _authContext = authContext;
         }
 
-        public object Get(GetUpcomingEpisodes request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
-            var options = GetDtoOptions(_authContext, request);
-
-            var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[] { typeof(Episode).Name },
-                OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
-                MinPremiereDate = minPremiereDate,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = options
-
-            });
-
-            var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = itemsResult.Count,
-                Items = returnItems
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetNextUpEpisodes request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var result = _tvSeriesManager.GetNextUp(new NextUpQuery
-            {
-                Limit = request.Limit,
-                ParentId = request.ParentId,
-                SeriesId = request.SeriesId,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            }, options);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
-
-            return ToOptimizedResult(new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = result.TotalRecordCount,
-                Items = returnItems
-            });
-        }
-
         /// <summary>
         /// Applies the paging.
         /// </summary>

From c4f9112b0dae98a2c80d4126ca9dcd82a2271835 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 23 Jun 2020 11:48:37 -0600
Subject: [PATCH 250/463] Move LiveTvService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/LiveTvController.cs  |  64 ++-
 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs |  84 +++
 .../LiveTvDtos/ChannelMappingOptionsDto.cs    |  10 +-
 MediaBrowser.Api/LiveTv/LiveTvService.cs      | 487 ------------------
 .../LiveTv/ProgressiveFileCopier.cs           |  80 ---
 5 files changed, 150 insertions(+), 575 deletions(-)
 create mode 100644 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
 delete mode 100644 MediaBrowser.Api/LiveTv/LiveTvService.cs
 delete mode 100644 MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs

diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 1279d42997..1887dbbc27 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using System.Linq;
 using System.Net.Mime;
 using System.Security.Cryptography;
@@ -15,7 +16,6 @@ using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -26,6 +26,7 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -43,7 +44,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IHttpClient _httpClient;
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
         private readonly ISessionContext _sessionContext;
         private readonly IStreamHelper _streamHelper;
         private readonly IMediaSourceManager _mediaSourceManager;
@@ -57,7 +57,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
-        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
         /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
@@ -68,7 +67,6 @@ namespace Jellyfin.Api.Controllers
             IHttpClient httpClient,
             ILibraryManager libraryManager,
             IDtoService dtoService,
-            IAuthorizationContext authContext,
             ISessionContext sessionContext,
             IStreamHelper streamHelper,
             IMediaSourceManager mediaSourceManager,
@@ -79,7 +77,6 @@ namespace Jellyfin.Api.Controllers
             _httpClient = httpClient;
             _libraryManager = libraryManager;
             _dtoService = dtoService;
-            _authContext = authContext;
             _sessionContext = sessionContext;
             _streamHelper = streamHelper;
             _mediaSourceManager = mediaSourceManager;
@@ -782,6 +779,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Timers/{timerId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
         {
             AssertUserCanManageLiveTv();
@@ -873,6 +871,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("SeriesTimers/{timerId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> UpdateSeriesTimer([FromRoute] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
         {
             AssertUserCanManageLiveTv();
@@ -979,6 +978,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("ListingProviders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
             [FromQuery] bool validateLogin,
             [FromQuery] bool validateListings,
@@ -1133,6 +1133,60 @@ namespace Jellyfin.Api.Controllers
             return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
         }
 
+        /// <summary>
+        /// Gets a live tv recording stream.
+        /// </summary>
+        /// <param name="recordingId">Recording id.</param>
+        /// <response code="200">Recording stream returned.</response>
+        /// <response code="404">Recording not found.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the recording stream on success,
+        /// or a <see cref="NotFoundResult"/> if recording not found.
+        /// </returns>
+        [HttpGet("LiveRecordings/{recordingId}/stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId)
+        {
+            var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
+
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                return NotFound();
+            }
+
+            await using var memoryStream = new MemoryStream();
+            await new ProgressiveFileCopier(_streamHelper, path).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            return File(memoryStream, MimeTypes.GetMimeType(path));
+        }
+
+        /// <summary>
+        /// Gets a live tv channel stream.
+        /// </summary>
+        /// <param name="streamId">Stream id.</param>
+        /// <param name="container">Container type.</param>
+        /// <response code="200">Stream returned.</response>
+        /// <response code="404">Stream not found.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the channel stream on success,
+        /// or a <see cref="NotFoundResult"/> if stream not found.
+        /// </returns>
+        [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container)
+        {
+            var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
+            if (liveStreamInfo == null)
+            {
+                return NotFound();
+            }
+
+            await using var memoryStream = new MemoryStream();
+            await new ProgressiveFileCopier(_streamHelper, liveStreamInfo).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            return File(memoryStream, MimeTypes.GetMimeType("file." + container));
+        }
+
         private void AssertUserCanManageLiveTv()
         {
             var user = _sessionContext.GetUser(Request);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
new file mode 100644
index 0000000000..e8e6966f45
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -0,0 +1,84 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Progressive file copier.
+    /// </summary>
+    public class ProgressiveFileCopier
+    {
+        private readonly string? _path;
+        private readonly IDirectStreamProvider? _directStreamProvider;
+        private readonly IStreamHelper _streamHelper;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
+        /// </summary>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="path">Filepath to stream from.</param>
+        public ProgressiveFileCopier(IStreamHelper streamHelper, string path)
+        {
+            _path = path;
+            _streamHelper = streamHelper;
+            _directStreamProvider = null;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
+        /// </summary>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
+        public ProgressiveFileCopier(IStreamHelper streamHelper, IDirectStreamProvider directStreamProvider)
+        {
+            _directStreamProvider = directStreamProvider;
+            _streamHelper = streamHelper;
+            _path = null;
+        }
+
+        /// <summary>
+        /// Write source stream to output.
+        /// </summary>
+        /// <param name="outputStream">Output stream.</param>
+        /// <param name="cancellationToken">Cancellation token.</param>
+        /// <returns>A <see cref="Task"/>.</returns>
+        public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
+        {
+            if (_directStreamProvider != null)
+            {
+                await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
+                return;
+            }
+
+            var fileOptions = FileOptions.SequentialScan;
+
+            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+            {
+                fileOptions |= FileOptions.Asynchronous;
+            }
+
+            await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions);
+            const int emptyReadLimit = 100;
+            var eofCount = 0;
+            while (eofCount < emptyReadLimit)
+            {
+                var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+
+                if (bytesRead == 0)
+                {
+                    eofCount++;
+                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    eofCount = 0;
+                }
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
index 642b40e4d2..970d8acdbc 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 
@@ -12,17 +13,20 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// <summary>
         /// Gets or sets list of tuner channels.
         /// </summary>
-        public List<TunerChannelMapping> TunerChannels { get; set; }
+        [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "TunerChannels", Justification = "Imported from ServiceStack")]
+        public List<TunerChannelMapping> TunerChannels { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets list of provider channels.
         /// </summary>
-        public List<NameIdPair> ProviderChannels { get; set; }
+        [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "ProviderChannels", Justification = "Imported from ServiceStack")]
+        public List<NameIdPair> ProviderChannels { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets list of mappings.
         /// </summary>
-        public NameValuePair[] Mappings { get; set; }
+        [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "Mappings", Justification = "Imported from ServiceStack")]
+        public NameValuePair[] Mappings { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets provider name.
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs
deleted file mode 100644
index 14abdcc998..0000000000
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ /dev/null
@@ -1,487 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Api.LiveTv
-{
-    [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
-    public class GetLiveStreamFile
-    {
-        public string Id { get; set; }
-        public string Container { get; set; }
-    }
-
-    [Route("/LiveTv/LiveRecordings/{Id}/stream", "GET", Summary = "Gets a live tv channel")]
-    public class GetLiveRecordingFile
-    {
-        public string Id { get; set; }
-    }
-
-    public class LiveTvService : BaseApiService
-    {
-        private readonly ILiveTvManager _liveTvManager;
-        private readonly IUserManager _userManager;
-        private readonly IHttpClient _httpClient;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly ISessionContext _sessionContext;
-        private readonly IStreamHelper _streamHelper;
-        private readonly IMediaSourceManager _mediaSourceManager;
-
-        public LiveTvService(
-            ILogger<LiveTvService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IMediaSourceManager mediaSourceManager,
-            IStreamHelper streamHelper,
-            ILiveTvManager liveTvManager,
-            IUserManager userManager,
-            IHttpClient httpClient,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            ISessionContext sessionContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _mediaSourceManager = mediaSourceManager;
-            _streamHelper = streamHelper;
-            _liveTvManager = liveTvManager;
-            _userManager = userManager;
-            _httpClient = httpClient;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _sessionContext = sessionContext;
-        }
-
-        public object Get(GetTunerHostTypes request)
-        {
-            var list = _liveTvManager.GetTunerHostTypes();
-            return ToOptimizedResult(list);
-        }
-
-        public object Get(GetLiveRecordingFile request)
-        {
-            var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
-
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                throw new FileNotFoundException();
-            }
-
-            var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path)
-            };
-
-            return new ProgressiveFileCopier(_streamHelper, path, outputHeaders, Logger)
-            {
-                AllowEndOfFile = false
-            };
-        }
-
-        public async Task<object> Get(DiscoverTuners request)
-        {
-            var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetLiveStreamFile request)
-        {
-            var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            var directStreamProvider = liveStreamInfo;
-
-            var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container)
-            };
-
-            return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger)
-            {
-                AllowEndOfFile = false
-            };
-        }
-
-        public object Get(GetDefaultListingProvider request)
-        {
-            return ToOptimizedResult(new ListingsProviderInfo());
-        }
-
-        public async Task<object> Post(SetChannelMapping request)
-        {
-            return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelId, request.ProviderChannelId).ConfigureAwait(false);
-        }
-
-        public async Task<object> Get(GetChannelMappingOptions request)
-        {
-            var config = GetConfiguration();
-
-            var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(request.ProviderId, i.Id, StringComparison.OrdinalIgnoreCase));
-
-            var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
-
-            var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(request.ProviderId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-            var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(request.ProviderId, CancellationToken.None)
-                     .ConfigureAwait(false);
-
-            var mappings = listingsProviderInfo.ChannelMappings;
-
-            var result = new ChannelMappingOptions
-            {
-                TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
-
-                ProviderChannels = providerChannels.Select(i => new NameIdPair
-                {
-                    Name = i.Name,
-                    Id = i.Id
-
-                }).ToList(),
-
-                Mappings = mappings,
-
-                ProviderName = listingsProviderName
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetSchedulesDirectCountries request)
-        {
-            // https://json.schedulesdirect.org/20141201/available/countries
-
-            var response = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = "https://json.schedulesdirect.org/20141201/available/countries",
-                BufferContent = false
-
-            }).ConfigureAwait(false);
-
-            return ResultFactory.GetResult(Request, response, "application/json");
-        }
-
-        private void AssertUserCanManageLiveTv()
-        {
-            var user = _sessionContext.GetUser(Request);
-
-            if (user == null)
-            {
-                throw new SecurityException("Anonymous live tv management is not allowed.");
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
-            {
-                throw new SecurityException("The current user does not have permission to manage live tv.");
-            }
-        }
-
-        public async Task<object> Post(AddListingProvider request)
-        {
-            if (request.Pw != null)
-            {
-                request.Password = GetHashedString(request.Pw);
-            }
-
-            request.Pw = null;
-
-            var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the hashed string.
-        /// </summary>
-        private string GetHashedString(string str)
-        {
-            // SchedulesDirect requires a SHA1 hash of the user's password
-            // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
-            using SHA1 sha = SHA1.Create();
-
-            return Hex.Encode(
-                sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
-        }
-
-        public void Delete(DeleteListingProvider request)
-        {
-            _liveTvManager.DeleteListingsProvider(request.Id);
-        }
-
-        public async Task<object> Post(AddTunerHost request)
-        {
-            var result = await _liveTvManager.SaveTunerHost(request).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        public void Delete(DeleteTunerHost request)
-        {
-            var config = GetConfiguration();
-
-            config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            ServerConfigurationManager.SaveConfiguration("livetv", config);
-        }
-
-        private LiveTvOptions GetConfiguration()
-        {
-            return ServerConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
-        }
-
-        private void UpdateConfiguration(LiveTvOptions options)
-        {
-            ServerConfigurationManager.SaveConfiguration("livetv", options);
-        }
-
-        public async Task<object> Get(GetLineups request)
-        {
-            var info = await _liveTvManager.GetLineups(request.Type, request.Id, request.Country, request.Location).ConfigureAwait(false);
-
-            return ToOptimizedResult(info);
-        }
-
-        private void RemoveFields(DtoOptions options)
-        {
-            var fields = options.Fields.ToList();
-
-            fields.Remove(ItemFields.CanDelete);
-            fields.Remove(ItemFields.CanDownload);
-            fields.Remove(ItemFields.DisplayPreferencesId);
-            fields.Remove(ItemFields.Etag);
-            options.Fields = fields.ToArray();
-        }
-
-        public async Task<object> Get(GetPrograms request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                ChannelIds = ApiEntryPoint.Split(request.ChannelIds, ',', true).Select(i => new Guid(i)).ToArray(),
-                HasAired = request.HasAired,
-                IsAiring = request.IsAiring,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            if (!string.IsNullOrEmpty(request.MinStartDate))
-            {
-                query.MinStartDate = DateTime.Parse(request.MinStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinEndDate))
-            {
-                query.MinEndDate = DateTime.Parse(request.MinEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxStartDate))
-            {
-                query.MaxStartDate = DateTime.Parse(request.MaxStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxEndDate))
-            {
-                query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            query.StartIndex = request.StartIndex;
-            query.Limit = request.Limit;
-            query.OrderBy = BaseItemsRequest.GetOrderBy(request.SortBy, request.SortOrder);
-            query.IsNews = request.IsNews;
-            query.IsMovie = request.IsMovie;
-            query.IsSeries = request.IsSeries;
-            query.IsKids = request.IsKids;
-            query.IsSports = request.IsSports;
-            query.SeriesTimerId = request.SeriesTimerId;
-            query.Genres = (request.Genres ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-            query.GenreIds = GetGuids(request.GenreIds);
-
-            if (!request.LibrarySeriesId.Equals(Guid.Empty))
-            {
-                query.IsSeries = true;
-
-                if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series)
-                {
-                    query.Name = series.Name;
-                }
-            }
-
-            var result = await _liveTvManager.GetPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetRecommendedPrograms request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IsAiring = request.IsAiring,
-                Limit = request.Limit,
-                HasAired = request.HasAired,
-                IsSeries = request.IsSeries,
-                IsMovie = request.IsMovie,
-                IsKids = request.IsKids,
-                IsNews = request.IsNews,
-                IsSports = request.IsSports,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            query.GenreIds = GetGuids(request.GenreIds);
-
-            var result = _liveTvManager.GetRecommendedPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Post(GetPrograms request)
-        {
-            return Get(request);
-        }
-
-        public void Delete(DeleteRecording request)
-        {
-            AssertUserCanManageLiveTv();
-
-            _libraryManager.DeleteItem(_libraryManager.GetItemById(request.Id), new DeleteOptions
-            {
-                DeleteFileLocation = false
-            });
-        }
-
-        public Task Delete(CancelTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CancelTimer(request.Id);
-        }
-
-        public Task Post(UpdateTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.UpdateTimer(request, CancellationToken.None);
-        }
-
-        public async Task<object> Get(GetSeriesTimers request)
-        {
-            var result = await _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
-            {
-                SortOrder = request.SortOrder,
-                SortBy = request.SortBy
-
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetSeriesTimer request)
-        {
-            var result = await _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task Delete(CancelSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CancelSeriesTimer(request.Id);
-        }
-
-        public Task Post(UpdateSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.UpdateSeriesTimer(request, CancellationToken.None);
-        }
-
-        public async Task<object> Get(GetDefaultTimer request)
-        {
-            if (string.IsNullOrEmpty(request.ProgramId))
-            {
-                var result = await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-            else
-            {
-                var result = await _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-        }
-
-        public async Task<object> Get(GetProgram request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var result = await _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task Post(CreateSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CreateSeriesTimer(request, CancellationToken.None);
-        }
-
-        public Task Post(CreateTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CreateTimer(request, CancellationToken.None);
-        }
-
-        public object Get(GetRecordingGroups request)
-        {
-            return ToOptimizedResult(new QueryResult<BaseItemDto>());
-        }
-
-        public object Get(GetRecordingGroup request)
-        {
-            throw new FileNotFoundException();
-        }
-
-        public object Get(GetGuideInfo request)
-        {
-            return ToOptimizedResult(_liveTvManager.GetGuideInfo());
-        }
-
-        public Task Post(ResetTuner request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.ResetTuner(request.Id, CancellationToken.None);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs b/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs
deleted file mode 100644
index 4c608d9a33..0000000000
--- a/MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.LiveTv
-{
-    public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
-    {
-        private readonly ILogger _logger;
-        private readonly string _path;
-        private readonly Dictionary<string, string> _outputHeaders;
-
-        public bool AllowEndOfFile = true;
-
-        private readonly IDirectStreamProvider _directStreamProvider;
-        private IStreamHelper _streamHelper;
-
-        public ProgressiveFileCopier(IStreamHelper streamHelper, string path, Dictionary<string, string> outputHeaders, ILogger logger)
-        {
-            _path = path;
-            _outputHeaders = outputHeaders;
-            _logger = logger;
-            _streamHelper = streamHelper;
-        }
-
-        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, IStreamHelper streamHelper, Dictionary<string, string> outputHeaders, ILogger logger)
-        {
-            _directStreamProvider = directStreamProvider;
-            _outputHeaders = outputHeaders;
-            _logger = logger;
-            _streamHelper = streamHelper;
-        }
-
-        public IDictionary<string, string> Headers => _outputHeaders;
-
-        public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
-        {
-            if (_directStreamProvider != null)
-            {
-                await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
-                return;
-            }
-
-            var fileOptions = FileOptions.SequentialScan;
-
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
-            {
-                fileOptions |= FileOptions.Asynchronous;
-            }
-
-            using (var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions))
-            {
-                var emptyReadLimit = AllowEndOfFile ? 20 : 100;
-                var eofCount = 0;
-                while (eofCount < emptyReadLimit)
-                {
-                    int bytesRead;
-                    bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-
-                    if (bytesRead == 0)
-                    {
-                        eofCount++;
-                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        eofCount = 0;
-                    }
-                }
-            }
-        }
-    }
-}

From 7e94bb786432536e95f4e76ea1f8fe02dd292fef Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 23 Jun 2020 11:53:08 -0600
Subject: [PATCH 251/463] fix controller attribute

---
 Jellyfin.Api/Controllers/LiveTvController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 1887dbbc27..aca295419e 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1000,7 +1000,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">Listing provider id.</param>
         /// <response code="204">Listing provider deleted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpGet("ListingProviders")]
+        [HttpDelete("ListingProviders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DeleteListingProvider([FromQuery] string id)

From d64770bcdbb3d5c1da168effbae03f296c2d0d99 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 24 Jun 2020 14:40:37 +0200
Subject: [PATCH 252/463] Finish TvShowsController

---
 Jellyfin.Api/Controllers/TvShowsController.cs | 242 +++++++++++++-
 MediaBrowser.Api/TvShowsService.cs            | 309 ------------------
 2 files changed, 225 insertions(+), 326 deletions(-)
 delete mode 100644 MediaBrowser.Api/TvShowsService.cs

diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index ba3c9fd66a..bd950b39fd 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -1,17 +1,21 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -27,7 +31,6 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
         private readonly ITVSeriesManager _tvSeriesManager;
-        private readonly IAuthorizationContext _authContext;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TvShowsController"/> class.
@@ -36,19 +39,16 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
         /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
-        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         public TvShowsController(
             IUserManager userManager,
             ILibraryManager libraryManager,
             IDtoService dtoService,
-            ITVSeriesManager tvSeriesManager,
-            IAuthorizationContext authContext)
+            ITVSeriesManager tvSeriesManager)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
             _dtoService = dtoService;
             _tvSeriesManager = tvSeriesManager;
-            _authContext = authContext;
         }
 
         /// <summary>
@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
         [HttpGet("NextUp")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
             [FromQuery] Guid userId,
             [FromQuery] int? startIndex,
@@ -76,14 +77,14 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
+            [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
             var options = new DtoOptions()
-                .AddItemFields(fields)
+                .AddItemFields(fields!)
                 .AddClientFields(Request)
-                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
             var result = _tvSeriesManager.GetNextUp(
                 new NextUpQuery
@@ -115,27 +116,24 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
-        /// <param name="seriesId">Optional. Filter by series id.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="enableImges">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
-        /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
         [HttpGet("Upcoming")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
             [FromQuery] Guid userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
-            [FromQuery] string? seriesId,
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] bool? enableUserData,
-            [FromQuery] bool enableTotalRecordCount = true)
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] bool? enableUserData)
         {
             var user = _userManager.GetUserById(userId);
 
@@ -144,9 +142,9 @@ namespace Jellyfin.Api.Controllers
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
 
             var options = new DtoOptions()
-                .AddItemFields(fields)
+                .AddItemFields(fields!)
                 .AddClientFields(Request)
-                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
             var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
@@ -168,5 +166,215 @@ namespace Jellyfin.Api.Controllers
                 Items = returnItems
             };
         }
+
+        /// <summary>
+        /// Gets episodes for a tv season.
+        /// </summary>
+        /// <param name="seriesId">The series id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="season">Optional filter by season number.</param>
+        /// <param name="seasonId">Optional. Filter by season id.</param>
+        /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="sortOrder">Optional. Sort order: Ascending,Descending.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
+        [HttpGet("{seriesId}/Episodes")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "sortOrder", Justification = "Imported from ServiceStack")]
+        public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
+            [FromRoute] string seriesId,
+            [FromQuery] Guid userId,
+            [FromQuery] string? fields,
+            [FromQuery] int? season,
+            [FromQuery] string? seasonId,
+            [FromQuery] bool? isMissing,
+            [FromQuery] string? adjacentTo,
+            [FromQuery] string? startItemId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] string? sortBy,
+            [FromQuery] SortOrder? sortOrder)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            List<BaseItem> episodes;
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields!)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+
+            if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id.
+            {
+                var item = _libraryManager.GetItemById(new Guid(seasonId));
+                if (!(item is Season seasonItem))
+                {
+                    return NotFound("No season exists with Id " + seasonId);
+                }
+
+                episodes = seasonItem.GetEpisodes(user, dtoOptions);
+            }
+            else if (season.HasValue) // Season number was supplied. Get episodes by season number
+            {
+                if (!(_libraryManager.GetItemById(seriesId) is Series series))
+                {
+                    return NotFound("Series not found");
+                }
+
+                var seasonItem = series
+                    .GetSeasons(user, dtoOptions)
+                    .FirstOrDefault(i => i.IndexNumber == season.Value);
+
+                episodes = seasonItem == null ?
+                    new List<BaseItem>()
+                    : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
+            }
+            else // No season number or season id was supplied. Returning all episodes.
+            {
+                if (!(_libraryManager.GetItemById(seriesId) is Series series))
+                {
+                    return NotFound("Series not found");
+                }
+
+                episodes = series.GetEpisodes(user, dtoOptions).ToList();
+            }
+
+            // Filter after the fact in case the ui doesn't want them
+            if (isMissing.HasValue)
+            {
+                var val = isMissing.Value;
+                episodes = episodes
+                    .Where(i => ((Episode)i).IsMissingEpisode == val)
+                    .ToList();
+            }
+
+            if (!string.IsNullOrWhiteSpace(startItemId))
+            {
+                episodes = episodes
+                    .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase))
+                    .ToList();
+            }
+
+            // This must be the last filter
+            if (!string.IsNullOrEmpty(adjacentTo))
+            {
+                episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList();
+            }
+
+            if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+            {
+                episodes.Shuffle();
+            }
+
+            var returnItems = episodes;
+
+            if (startIndex.HasValue || limit.HasValue)
+            {
+                returnItems = ApplyPaging(episodes, startIndex, limit).ToList();
+            }
+
+            var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = episodes.Count,
+                Items = dtos
+            };
+        }
+
+        /// <summary>
+        /// Gets seasons for a tv series.
+        /// </summary>
+        /// <param name="seriesId">The series id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="isSpecialSeason">Optional. Filter by special season.</param>
+        /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
+        [HttpGet("{seriesId}/Seasons")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
+            [FromRoute] string seriesId,
+            [FromQuery] Guid userId,
+            [FromQuery] string fields,
+            [FromQuery] bool? isSpecialSeason,
+            [FromQuery] bool? isMissing,
+            [FromQuery] string adjacentTo,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] bool? enableUserData)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            if (!(_libraryManager.GetItemById(seriesId) is Series series))
+            {
+                return NotFound("Series not found");
+            }
+
+            var seasons = series.GetItemList(new InternalItemsQuery(user)
+            {
+                IsMissing = isMissing,
+                IsSpecialSeason = isSpecialSeason,
+                AdjacentTo = adjacentTo
+            });
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+
+            var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = returnItems.Count,
+                Items = returnItems
+            };
+        }
+
+        /// <summary>
+        /// Applies the paging.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="startIndex">The start index.</param>
+        /// <param name="limit">The limit.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
+        {
+            // Start at
+            if (startIndex.HasValue)
+            {
+                items = items.Skip(startIndex.Value);
+            }
+
+            // Return limit
+            if (limit.HasValue)
+            {
+                items = items.Take(limit.Value);
+            }
+
+            return items;
+        }
     }
 }
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
deleted file mode 100644
index ab2a2ddaa0..0000000000
--- a/MediaBrowser.Api/TvShowsService.cs
+++ /dev/null
@@ -1,309 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Shows/{Id}/Episodes", "GET", Summary = "Gets episodes for a tv season")]
-    public class GetEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Season", Description = "Optional filter by season number.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public int? Season { get; set; }
-
-        [ApiMember(Name = "SeasonId", Description = "Optional. Filter by season id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeasonId { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "StartItemId", Description = "Optional. Skip through the list until a given item is found.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string StartItemId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public SortOrder? SortOrder { get; set; }
-    }
-
-    [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")]
-    public class GetSeasons : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "IsSpecialSeason", Description = "Optional. Filter by special season.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsSpecialSeason { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
-    /// <summary>
-    /// Class TvShowsService
-    /// </summary>
-    [Authenticated]
-    public class TvShowsService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="TvShowsService" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="userDataManager">The user data repository.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public TvShowsService(
-            ILogger<TvShowsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Applies the paging.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <param name="startIndex">The start index.</param>
-        /// <param name="limit">The limit.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
-        {
-            // Start at
-            if (startIndex.HasValue)
-            {
-                items = items.Skip(startIndex.Value);
-            }
-
-            // Return limit
-            if (limit.HasValue)
-            {
-                items = items.Take(limit.Value);
-            }
-
-            return items;
-        }
-
-        public object Get(GetSeasons request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var series = GetSeries(request.Id);
-
-            if (series == null)
-            {
-                throw new ResourceNotFoundException("Series not found");
-            }
-
-            var seasons = series.GetItemList(new InternalItemsQuery(user)
-            {
-                IsMissing = request.IsMissing,
-                IsSpecialSeason = request.IsSpecialSeason,
-                AdjacentTo = request.AdjacentTo
-
-            });
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = returnItems.Count,
-                Items = returnItems
-            };
-        }
-
-        private Series GetSeries(string seriesId)
-        {
-            if (!string.IsNullOrWhiteSpace(seriesId))
-            {
-                return _libraryManager.GetItemById(seriesId) as Series;
-            }
-
-            return null;
-        }
-
-        public object Get(GetEpisodes request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            List<BaseItem> episodes;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            if (!string.IsNullOrWhiteSpace(request.SeasonId))
-            {
-                if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season))
-                {
-                    throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
-                }
-
-                episodes = season.GetEpisodes(user, dtoOptions);
-            }
-            else if (request.Season.HasValue)
-            {
-                var series = GetSeries(request.Id);
-
-                if (series == null)
-                {
-                    throw new ResourceNotFoundException("Series not found");
-                }
-
-                var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value);
-
-                episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions);
-            }
-            else
-            {
-                var series = GetSeries(request.Id);
-
-                if (series == null)
-                {
-                    throw new ResourceNotFoundException("Series not found");
-                }
-
-                episodes = series.GetEpisodes(user, dtoOptions).ToList();
-            }
-
-            // Filter after the fact in case the ui doesn't want them
-            if (request.IsMissing.HasValue)
-            {
-                var val = request.IsMissing.Value;
-                episodes = episodes.Where(i => ((Episode)i).IsMissingEpisode == val).ToList();
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.StartItemId))
-            {
-                episodes = episodes.SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), request.StartItemId, StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-
-            // This must be the last filter
-            if (!string.IsNullOrEmpty(request.AdjacentTo))
-            {
-                episodes = UserViewBuilder.FilterForAdjacency(episodes, request.AdjacentTo).ToList();
-            }
-
-            if (string.Equals(request.SortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
-            {
-                episodes.Shuffle();
-            }
-
-            var returnItems = episodes;
-
-            if (request.StartIndex.HasValue || request.Limit.HasValue)
-            {
-                returnItems = ApplyPaging(episodes, request.StartIndex, request.Limit).ToList();
-            }
-
-            var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = episodes.Count,
-                Items = dtos
-            };
-        }
-    }
-}

From 32881c6b22da62652adaf0cfaf4c7702aa3c616b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 08:01:12 -0600
Subject: [PATCH 253/463] add missing docs and attributes

---
 Jellyfin.Api/Controllers/LibraryController.cs | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 92843a3737..9ad70024a2 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -31,6 +31,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
@@ -102,6 +103,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
         [HttpGet("/Items/{itemId}/File")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult GetFile([FromRoute] Guid itemId)
         {
             var item = _libraryManager.GetItemById(itemId);
@@ -128,6 +131,7 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews(
             [FromRoute] Guid itemId,
             [FromQuery] int? startIndex,
@@ -147,6 +151,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The item theme songs.</returns>
         [HttpGet("/Items/{itemId}/ThemeSongs")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<ThemeMediaResult> GetThemeSongs(
             [FromRoute] Guid itemId,
             [FromQuery] Guid userId,
@@ -211,6 +217,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The item theme videos.</returns>
         [HttpGet("/Items/{itemId}/ThemeVideos")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<ThemeMediaResult> GetThemeVideos(
             [FromRoute] Guid itemId,
             [FromQuery] Guid userId,
@@ -273,6 +281,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs and videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
             [FromRoute] Guid itemId,
             [FromQuery] Guid userId,
@@ -303,6 +312,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpGet("/Library/Refresh")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RefreshLibrary()
         {
             try
@@ -326,6 +336,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("/Items/{itemId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
         public ActionResult DeleteItem(Guid itemId)
         {
             var item = _libraryManager.GetItemById(itemId);
@@ -354,6 +366,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("/Items")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
         public ActionResult DeleteItems([FromQuery] string ids)
         {
             var itemIds = string.IsNullOrWhiteSpace(ids)
@@ -394,6 +408,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Item counts.</returns>
         [HttpGet("/Items/Counts")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ItemCounts> GetItemCounts(
             [FromQuery] Guid userId,
             [FromQuery] bool? isFavorite)
@@ -427,6 +442,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Item parents.</returns>
         [HttpGet("/Items/{itemId}/Ancestors")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid userId)
         {
             var item = _libraryManager.GetItemById(itemId);
@@ -467,6 +484,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>List of physical paths.</returns>
         [HttpGet("/Library/PhysicalPaths")]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<string>> GetPhysicalPaths()
         {
             return Ok(_libraryManager.RootFolder.Children
@@ -477,9 +495,11 @@ namespace Jellyfin.Api.Controllers
         /// Gets all user media folders.
         /// </summary>
         /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
+        /// <response code="200">Media folders returned.</response>
         /// <returns>List of user media folders.</returns>
         [HttpGet("/Library/MediaFolders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
         {
             var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
@@ -510,6 +530,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Library/Series/Added")]
         [HttpPost("/Library/Series/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedSeries([FromQuery] string tvdbId)
         {
             var series = _libraryManager.GetItemList(new InternalItemsQuery
@@ -539,6 +560,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Library/Movies/Added")]
         [HttpPost("/Library/Movies/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedMovies([FromRoute] string tmdbId, [FromRoute] string imdbId)
         {
             var movies = _libraryManager.GetItemList(new InternalItemsQuery
@@ -579,6 +601,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates)
         {
             foreach (var item in updates)
@@ -599,6 +622,8 @@ namespace Jellyfin.Api.Controllers
         /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
         [HttpGet("/Items/{itemId}/Download")]
         [Authorize(Policy = Policies.Download)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult GetDownload([FromRoute] Guid itemId)
         {
             var item = _libraryManager.GetItemById(itemId);
@@ -662,6 +687,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <response code="200">Similar items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
         [HttpGet("/Artists/{itemId}/Similar")]
         [HttpGet("/Items/{itemId}/Similar")]
@@ -673,6 +699,7 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
             [FromQuery] string excludeArtistIds,
@@ -728,6 +755,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Library options info.</returns>
         [HttpGet("/Libraries/AvailableOptions")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo([FromQuery] string libraryContentType, [FromQuery] bool isNewLibrary)
         {
             var result = new LibraryOptionsResultDto();

From fee07219d0f054e33a2a60c84bd66344b42e0c0e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 10:54:25 -0600
Subject: [PATCH 254/463] fix merge

---
 .../Controllers/ChannelsController.cs         |  7 +++--
 Jellyfin.Api/Extensions/DtoExtensions.cs      |  2 +-
 Jellyfin.Api/Helpers/RequestHelpers.cs        | 27 ++-----------------
 3 files changed, 8 insertions(+), 28 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index a22cdd7803..c3e29323e3 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Dto;
@@ -130,7 +131,8 @@ namespace Jellyfin.Api.Controllers
                 ChannelIds = new[] { channelId },
                 ParentId = folderId ?? Guid.Empty,
                 OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
-                DtoOptions = new DtoOptions { Fields = RequestHelpers.GetItemFields(fields) }
+                DtoOptions = new DtoOptions()
+                    .AddItemFields(fields)
             };
 
             foreach (var filter in RequestHelpers.GetFilters(filters))
@@ -206,7 +208,8 @@ namespace Jellyfin.Api.Controllers
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Select(i => new Guid(i))
                     .ToArray(),
-                DtoOptions = new DtoOptions { Fields = RequestHelpers.GetItemFields(fields) }
+                DtoOptions = new DtoOptions()
+                    .AddItemFields(fields)
             };
 
             foreach (var filter in RequestHelpers.GetFilters(filters))
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index ac248109d7..4c495404f8 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Extensions
         /// <param name="dtoOptions">DtoOptions object.</param>
         /// <param name="fields">Comma delimited string of fields.</param>
         /// <returns>Modified DtoOptions object.</returns>
-        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields)
+        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields)
         {
             if (string.IsNullOrEmpty(fields))
             {
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 6e88823914..242f884672 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -2,10 +2,10 @@
 using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
@@ -55,29 +55,6 @@ namespace Jellyfin.Api.Helpers
             return result;
         }
 
-        /// <summary>
-        /// Gets the item fields.
-        /// </summary>
-        /// <param name="fields">The fields.</param>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        public static ItemFields[] GetItemFields(string? fields)
-        {
-            if (string.IsNullOrEmpty(fields))
-            {
-                return Array.Empty<ItemFields>();
-            }
-
-            return fields.Split(',').Select(v =>
-            {
-                if (Enum.TryParse(v, true, out ItemFields value))
-                {
-                    return (ItemFields?)value;
-                }
-
-                return null;
-            }).Where(i => i.HasValue).Select(i => i!.Value).ToArray();
-        }
-
         /// <summary>
         /// Get parsed filters.
         /// </summary>

From 60ab4cd7b12725d698edfdc07292d8d6e5cded5c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 12:23:46 -0600
Subject: [PATCH 255/463] move YearsService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/YearsController.cs  | 226 +++++++++++++++++++
 MediaBrowser.Api/UserLibrary/YearsService.cs |  54 -----
 2 files changed, 226 insertions(+), 54 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/YearsController.cs

diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
new file mode 100644
index 0000000000..2d8ef69cbf
--- /dev/null
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -0,0 +1,226 @@
+using System;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    public class YearsController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Get years.
+        /// </summary>
+        /// <param name="maxOfficialRating">Optional. Filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="hasThemeSong">Optional. Filter by items with theme songs.</param>
+        /// <param name="hasThemeVideo">Optional. Filter by items with theme videos.</param>
+        /// <param name="hasSubtitles">Optional. Filter by items with subtitles.</param>
+        /// <param name="hasSpecialFeatures">Optional. Filter by items with special features.</param>
+        /// <param name="hasTrailer">Optional. Filter by items with trailers.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="minIndexNumber">Optional. Filter by minimum index number.</param>
+        /// <param name="parentIndexNumber">Optional. Filter by parent index number.</param>
+        /// <param name="hasParentalRating">Optional. filter by items that have or do not have a parental rating.</param>
+        /// <param name="isHd"></param>
+        /// <param name="is4k"></param>
+        /// <param name="locationTypes"></param>
+        /// <param name="excludeLocationTypes"></param>
+        /// <param name="isMissing"></param>
+        /// <param name="isUnaired"></param>
+        /// <param name="minCommunityRating"></param>
+        /// <param name="minCriticRating"></param>
+        /// <param name="airedDuringSeason"></param>
+        /// <param name="minPremiereDate"></param>
+        /// <param name="minDateLastSaved"></param>
+        /// <param name="minDateLastSavedForUser"></param>
+        /// <param name="maxPremiereDate"></param>
+        /// <param name="hasOverview"></param>
+        /// <param name="hasImdbId"></param>
+        /// <param name="hasTmdbId"></param>
+        /// <param name="hasTvdbId"></param>
+        /// <param name="excludeItemIds"></param>
+        /// <param name="startIndex"></param>
+        /// <param name="limit"></param>
+        /// <param name="searchTerm"></param>
+        /// <param name="sortOrder"></param>
+        /// <param name="parentId"></param>
+        /// <param name="fields"></param>
+        /// <param name="excludeItemTypes"></param>
+        /// <param name="includeItemTypes"></param>
+        /// <param name="filters"></param>
+        /// <param name="isFavorite"></param>
+        /// <param name="mediaTypes"></param>
+        /// <param name="imageTypes"></param>
+        /// <param name="sortBy"></param>
+        /// <param name="isPlayed"></param>
+        /// <param name="genres"></param>
+        /// <param name="genreIds"></param>
+        /// <param name="officialRatings"></param>
+        /// <param name="tags"></param>
+        /// <param name="years"></param>
+        /// <param name="enableUserData"></param>
+        /// <param name="imageTypeLimit"></param>
+        /// <param name="enableImageTypes"></param>
+        /// <param name="person"></param>
+        /// <param name="personIds"></param>
+        /// <param name="personTypes"></param>
+        /// <param name="studios"></param>
+        /// <param name="studioIds"></param>
+        /// <param name="artists"></param>
+        /// <param name="excludeArtistIds"></param>
+        /// <param name="artistIds"></param>
+        /// <param name="albumArtistIds"></param>
+        /// <param name="contributingArtistIds"></param>
+        /// <param name="albums"></param>
+        /// <param name="albumIds"></param>
+        /// <param name="ids"></param>
+        /// <param name="videoTypes"></param>
+        /// <param name="userId"></param>
+        /// <param name="minOfficialRating"></param>
+        /// <param name="isLocked"></param>
+        /// <param name="isPlaceholder"></param>
+        /// <param name="hasOfficialRating"></param>
+        /// <param name="collapseBoxSetItems"></param>
+        /// <param name="minWidth"></param>
+        /// <param name="minHeight"></param>
+        /// <param name="maxWidth"></param>
+        /// <param name="maxHeight"></param>
+        /// <param name="is3d"></param>
+        /// <param name="seriesStatus"></param>
+        /// <param name="nameStartsWithOrGreater"></param>
+        /// <param name="nameStartsWith"></param>
+        /// <param name="nameLessThan"></param>
+        /// <param name="recursive"></param>
+        /// <param name="enableImages"></param>
+        /// <param name="enableTotalRecordCount"></param>
+        /// <returns></returns>
+        [HttpGet]
+        public ActionResult<QueryResult<BaseItemDto>> GetYears(
+            [FromQuery] string maxOfficialRating,
+            [FromQuery] bool? hasThemeSong,
+            [FromQuery] bool? hasThemeVideo,
+            [FromQuery] bool? hasSubtitles,
+            [FromQuery] bool? hasSpecialFeatures,
+            [FromQuery] bool? hasTrailer,
+            [FromQuery] string adjacentTo,
+            [FromQuery] int? minIndexNumber,
+            [FromQuery] int? parentIndexNumber,
+            [FromQuery] bool? hasParentalRating,
+            [FromQuery] bool? isHd,
+            [FromQuery] bool? is4k,
+            [FromQuery] string locationTypes,
+            [FromQuery] string excludeLocationTypes,
+            [FromQuery] bool? isMissing,
+            [FromQuery] bool? isUnaired,
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] double? minCriticRating,
+            [FromQuery] int? airedDuringSeason,
+            [FromQuery] DateTime? minPremiereDate,
+            [FromQuery] DateTime? minDateLastSaved,
+            [FromQuery] DateTime? minDateLastSavedForUser,
+            [FromQuery] DateTime? maxPremiereDate,
+            [FromQuery] bool? hasOverview,
+            [FromQuery] bool? hasImdbId,
+            [FromQuery] bool? hasTmdbId,
+            [FromQuery] bool? hasTvdbId,
+            [FromQuery] string excludeItemIds,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string sortOrder,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string imageTypes,
+            [FromQuery] string sortBy,
+            [FromQuery] bool? isPlayed,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] string artists,
+            [FromQuery] string excludeArtistIds,
+            [FromQuery] string artistIds,
+            [FromQuery] string albumArtistIds,
+            [FromQuery] string contributingArtistIds,
+            [FromQuery] string albums,
+            [FromQuery] string albumIds,
+            [FromQuery] string ids,
+            [FromQuery] string videoTypes,
+            [FromQuery] Guid userId,
+            [FromQuery] string minOfficialRating,
+            [FromQuery] bool? isLocked,
+            [FromQuery] bool? isPlaceholder,
+            [FromQuery] bool? hasOfficialRating,
+            [FromQuery] bool? collapseBoxSetItems,
+            [FromQuery] int? minWidth,
+            [FromQuery] int? minHeight,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] bool? is3d,
+            [FromQuery] string seriesStatus,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool recursive = true,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+
+        }
+
+        /// <summary>
+        /// Gets a year.
+        /// </summary>
+        /// <param name="year">The year.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Year returned.</response>
+        /// <response code="404">Year not found.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the year,
+        /// or a <see cref="NotFoundResult"/> if year not found.
+        /// </returns>
+        [HttpGet("{year}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid userId)
+        {
+            var item = _libraryManager.GetYear(year);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            var dtoOptions = new DtoOptions()
+                .AddClientFields(Request);
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs
index d023ee90ab..f34884f8bc 100644
--- a/MediaBrowser.Api/UserLibrary/YearsService.cs
+++ b/MediaBrowser.Api/UserLibrary/YearsService.cs
@@ -20,27 +20,6 @@ namespace MediaBrowser.Api.UserLibrary
     {
     }
 
-    /// <summary>
-    /// Class GetYear
-    /// </summary>
-    [Route("/Years/{Year}", "GET", Summary = "Gets a year")]
-    public class GetYear : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the year.
-        /// </summary>
-        /// <value>The year.</value>
-        [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Year { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
     /// <summary>
     /// Class YearsService
     /// </summary>
@@ -68,39 +47,6 @@ namespace MediaBrowser.Api.UserLibrary
         {
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetYear request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetYear request)
-        {
-            var item = LibraryManager.GetYear(request.Year);
-
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

From fb81f95ae866b51b67b14458fb4b9e1223f4c5fd Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 12:49:09 -0600
Subject: [PATCH 256/463] fix duplicate function

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 19 +------------------
 1 file changed, 1 insertion(+), 18 deletions(-)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 1b471ca548..446ee716aa 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -31,23 +31,6 @@ namespace Jellyfin.Api.Helpers
                 : value.Split(separator);
         }
 
-        /// <summary>
-        /// Splits a comma delimited string and parses Guids.
-        /// </summary>
-        /// <param name="value">Input value.</param>
-        /// <returns>Parsed Guids.</returns>
-        public static Guid[] GetGuids(string value)
-        {
-            if (value == null)
-            {
-                return Array.Empty<Guid>();
-            }
-
-            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(i => new Guid(i))
-                .ToArray();
-        }
-
         /// <summary>
         /// Checks if the user can update an entry.
         /// </summary>
@@ -104,7 +87,7 @@ namespace Jellyfin.Api.Helpers
                 return Array.Empty<Guid>();
             }
 
-            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+            return Split(value, ',', true)
                 .Select(i => new Guid(i))
                 .ToArray();
         }

From e72a22c5649d2bb3581b3ee5efd3dbbea370d89c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 13:48:22 -0600
Subject: [PATCH 257/463] Move YearsService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/YearsController.cs  | 361 +++++++++++++++----
 Jellyfin.Api/Helpers/RequestHelpers.cs       |  41 +++
 MediaBrowser.Api/UserLibrary/YearsService.cs |  77 ----
 3 files changed, 329 insertions(+), 150 deletions(-)
 delete mode 100644 MediaBrowser.Api/UserLibrary/YearsService.cs

diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 2d8ef69cbf..a480b78ead 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,6 +1,12 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
@@ -9,12 +15,31 @@ using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
 {
+    /// <summary>
+    /// Years controller.
+    /// </summary>
     public class YearsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDtoService _dtoService;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="YearsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public YearsController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+        }
+
         /// <summary>
         /// Get years.
         /// </summary>
@@ -28,80 +53,157 @@ namespace Jellyfin.Api.Controllers
         /// <param name="minIndexNumber">Optional. Filter by minimum index number.</param>
         /// <param name="parentIndexNumber">Optional. Filter by parent index number.</param>
         /// <param name="hasParentalRating">Optional. filter by items that have or do not have a parental rating.</param>
-        /// <param name="isHd"></param>
-        /// <param name="is4k"></param>
-        /// <param name="locationTypes"></param>
-        /// <param name="excludeLocationTypes"></param>
-        /// <param name="isMissing"></param>
-        /// <param name="isUnaired"></param>
-        /// <param name="minCommunityRating"></param>
-        /// <param name="minCriticRating"></param>
-        /// <param name="airedDuringSeason"></param>
-        /// <param name="minPremiereDate"></param>
-        /// <param name="minDateLastSaved"></param>
-        /// <param name="minDateLastSavedForUser"></param>
-        /// <param name="maxPremiereDate"></param>
-        /// <param name="hasOverview"></param>
-        /// <param name="hasImdbId"></param>
-        /// <param name="hasTmdbId"></param>
-        /// <param name="hasTvdbId"></param>
-        /// <param name="excludeItemIds"></param>
-        /// <param name="startIndex"></param>
-        /// <param name="limit"></param>
-        /// <param name="searchTerm"></param>
-        /// <param name="sortOrder"></param>
-        /// <param name="parentId"></param>
-        /// <param name="fields"></param>
-        /// <param name="excludeItemTypes"></param>
-        /// <param name="includeItemTypes"></param>
-        /// <param name="filters"></param>
-        /// <param name="isFavorite"></param>
-        /// <param name="mediaTypes"></param>
-        /// <param name="imageTypes"></param>
-        /// <param name="sortBy"></param>
-        /// <param name="isPlayed"></param>
-        /// <param name="genres"></param>
-        /// <param name="genreIds"></param>
-        /// <param name="officialRatings"></param>
-        /// <param name="tags"></param>
-        /// <param name="years"></param>
-        /// <param name="enableUserData"></param>
-        /// <param name="imageTypeLimit"></param>
-        /// <param name="enableImageTypes"></param>
-        /// <param name="person"></param>
-        /// <param name="personIds"></param>
-        /// <param name="personTypes"></param>
-        /// <param name="studios"></param>
-        /// <param name="studioIds"></param>
-        /// <param name="artists"></param>
-        /// <param name="excludeArtistIds"></param>
-        /// <param name="artistIds"></param>
-        /// <param name="albumArtistIds"></param>
-        /// <param name="contributingArtistIds"></param>
-        /// <param name="albums"></param>
-        /// <param name="albumIds"></param>
-        /// <param name="ids"></param>
-        /// <param name="videoTypes"></param>
-        /// <param name="userId"></param>
-        /// <param name="minOfficialRating"></param>
-        /// <param name="isLocked"></param>
-        /// <param name="isPlaceholder"></param>
-        /// <param name="hasOfficialRating"></param>
-        /// <param name="collapseBoxSetItems"></param>
-        /// <param name="minWidth"></param>
-        /// <param name="minHeight"></param>
-        /// <param name="maxWidth"></param>
-        /// <param name="maxHeight"></param>
-        /// <param name="is3d"></param>
-        /// <param name="seriesStatus"></param>
-        /// <param name="nameStartsWithOrGreater"></param>
-        /// <param name="nameStartsWith"></param>
-        /// <param name="nameLessThan"></param>
-        /// <param name="recursive"></param>
-        /// <param name="enableImages"></param>
-        /// <param name="enableTotalRecordCount"></param>
-        /// <returns></returns>
+        /// <param name="isHd">Optional. Filter by items that are HD or not.</param>
+        /// <param name="is4k">Optional. Filter by items that are 4K or not.</param>
+        /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+        /// <param name="excludeLocationTypes">Optional. If specified, results will be excluded based on LocationType. This allows multiple, comma delimited.</param>
+        /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+        /// <param name="isUnaired">Optional.  Filter by items that are unaired episodes or not.</param>
+        /// <param name="minCommunityRating">Optional. Filter by minimum community rating.</param>
+        /// <param name="minCriticRating">Optional. Filter by minimum critic rating.</param>
+        /// <param name="airedDuringSeason">Gets all episodes that aired during a season, including specials.</param>
+        /// <param name="minPremiereDate">Optional. The minimum premiere date.</param>
+        /// <param name="minDateLastSaved">Optional. The minimum last saved date.</param>
+        /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for user.</param>
+        /// <param name="maxPremiereDate">Optional. The maximum premiere date.</param>
+        /// <param name="hasOverview">Optional. Filter by items that have an overview or not.</param>
+        /// <param name="hasImdbId">Optional. Filter by items that have an imdb id or not.</param>
+        /// <param name="hasTmdbId">Optional. Filter by items that have a tmdb id or not.</param>
+        /// <param name="hasTvdbId">Optional. Filter by items that have a tvdb id or not.</param>
+        /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
+        /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">Optional. Search term.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional. Filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="isPlayed">Optional. Filter by items that are played, or not.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="artists">Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimited.</param>
+        /// <param name="excludeArtistIds">Optional. If specified, results will be excluded based on artist id. This allows multiple, pipe delimited.</param>
+        /// <param name="artistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
+        /// <param name="albumArtistIds">Optional. If specified, results will be filtered based on album artist id. This allows multiple, pipe delimited.</param>
+        /// <param name="contributingArtistIds">Optional. If specified, results will be filtered based on contributing artist id. This allows multiple, pipe delimited.</param>
+        /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+        /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
+        /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+        /// <param name="videoTypes">Optional. Filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
+        /// <param name="userId">User Id.</param>
+        /// <param name="minOfficialRating">Optional. Filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="isLocked">Optional. Filter by items that are locked.</param>
+        /// <param name="isPlaceholder">Optional. Filter by items that are placeholders.</param>
+        /// <param name="hasOfficialRating">Optional. Filter by items that have official ratings.</param>
+        /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+        /// <param name="minWidth">Min width.</param>
+        /// <param name="minHeight">Min height.</param>
+        /// <param name="maxWidth">Max width.</param>
+        /// <param name="maxHeight">Max height.</param>
+        /// <param name="is3d">Optional. Filter by items that are 3D, or not.</param>
+        /// <param name="seriesStatus">Optional. Filter by Series Status. Allows multiple, comma delimited.</param>
+        /// <param name="nameStartsWithOrGreater">Optional. Filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional. Filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional. Filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="recursive">Search recursively.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Return total record count.</param>
+        /// <response code="200">Year query returned.</response>
+        /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
         [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxOfficialRating", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeSong", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeVideo", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSubtitles", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSpecialFeatures", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTrailer", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "adjacentTo", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minIndexNumber", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "parentIndexNumber", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasParentalRating", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isHd", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is4k", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "locationTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeLocationTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isMissing", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isUnaired", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCommunityRating", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCriticRating", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "airedDuringSeason", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minPremiereDate", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSaved", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSavedForUser", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxPremiereDate", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOverview", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasImdbId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTmdbId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTvdbId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeItemIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "searchTerm", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "filters", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isFavorite", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlayed", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genres", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genreIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "officialRatings", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "tags", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "years", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "person", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albums", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumIds", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "videoTypes", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isLocked", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlaceholder", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOfficialRating", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "collapseBoxSetItems", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minWidth", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minHeight", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxWidth", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxHeight", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is3d", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesStatus", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWithOrGreater", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWith", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameLessThan", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<BaseItemDto>> GetYears(
             [FromQuery] string maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
@@ -186,7 +288,88 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            IList<BaseItem> items;
+
+            var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+            var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+            var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = excludeItemTypesArr,
+                IncludeItemTypes = includeItemTypesArr,
+                MediaTypes = mediaTypesArr,
+                DtoOptions = dtoOptions
+            };
+
+            bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
+
+            if (parentItem.IsFolder)
+            {
+                var folder = (Folder)parentItem;
+
+                if (!userId.Equals(Guid.Empty))
+                {
+                    items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
+                }
+                else
+                {
+                    items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
+                }
+            }
+            else
+            {
+                items = new[] { parentItem }.Where(Filter).ToList();
+            }
+
+            var extractedItems = GetAllItems(items);
 
+            var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
+
+            var ibnItemsArray = filteredItems.ToList();
+
+            IEnumerable<BaseItem> ibnItems = ibnItemsArray;
+
+            var result = new QueryResult<BaseItemDto> { TotalRecordCount = ibnItemsArray.Count };
+
+            if (startIndex.HasValue || limit.HasValue)
+            {
+                if (startIndex.HasValue)
+                {
+                    ibnItems = ibnItems.Skip(startIndex.Value);
+                }
+
+                if (limit.HasValue)
+                {
+                    ibnItems = ibnItems.Take(limit.Value);
+                }
+            }
+
+            var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
+
+            var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
+
+            result.Items = dtos.Where(i => i != null).ToArray();
+
+            return result;
         }
 
         /// <summary>
@@ -222,5 +405,37 @@ namespace Jellyfin.Api.Controllers
 
             return _dtoService.GetBaseItemDto(item, dtoOptions);
         }
+
+        private bool FilterItem(BaseItem f, IReadOnlyCollection<string> excludeItemTypes, IReadOnlyCollection<string> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
+        {
+            // Exclude item types
+            if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Include item types
+            if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Include MediaTypes
+            if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items)
+        {
+            return items
+                .Select(i => i.ProductionYear ?? 0)
+                .Where(i => i > 0)
+                .Distinct()
+                .Select(year => _libraryManager.GetYear(year));
+        }
     }
 }
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index e2a0cf4faf..6ba62b2e43 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -3,6 +3,7 @@ using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
@@ -91,5 +92,45 @@ namespace Jellyfin.Api.Helpers
                 .Select(i => new Guid(i))
                 .ToArray();
         }
+
+        /// <summary>
+        /// Get orderby.
+        /// </summary>
+        /// <param name="sortBy">Sort by.</param>
+        /// <param name="requestedSortOrder">Sort order.</param>
+        /// <returns>Resulting order by.</returns>
+        internal static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
+        {
+            var val = sortBy;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return Array.Empty<ValueTuple<string, SortOrder>>();
+            }
+
+            var vals = val.Split(',');
+            if (string.IsNullOrWhiteSpace(requestedSortOrder))
+            {
+                requestedSortOrder = "Ascending";
+            }
+
+            var sortOrders = requestedSortOrder.Split(',');
+
+            var result = new ValueTuple<string, SortOrder>[vals.Length];
+
+            for (var i = 0; i < vals.Length; i++)
+            {
+                var sortOrderIndex = sortOrders.Length > i ? i : 0;
+
+                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
+                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
+                    ? SortOrder.Descending
+                    : SortOrder.Ascending;
+
+                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
+            }
+
+            return result;
+        }
     }
 }
diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs
deleted file mode 100644
index f34884f8bc..0000000000
--- a/MediaBrowser.Api/UserLibrary/YearsService.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetYears
-    /// </summary>
-    [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")]
-    public class GetYears : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class YearsService
-    /// </summary>
-    [Authenticated]
-    public class YearsService : BaseItemsByNameService<Year>
-    {
-        public YearsService(
-            ILogger<YearsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetYears request)
-        {
-            var result = GetResult(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            return items
-                .Select(i => i.ProductionYear ?? 0)
-                .Where(i => i > 0)
-                .Distinct()
-                .Select(year => LibraryManager.GetYear(year));
-        }
-    }
-}

From 1228d6711e44d63eca9f1909a3844fa896ec4587 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 14:35:57 -0600
Subject: [PATCH 258/463] remove unused parameters

---
 Jellyfin.Api/Controllers/YearsController.cs | 212 +-------------------
 1 file changed, 1 insertion(+), 211 deletions(-)

diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index a480b78ead..a036c818c9 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
@@ -43,250 +42,41 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get years.
         /// </summary>
-        /// <param name="maxOfficialRating">Optional. Filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
-        /// <param name="hasThemeSong">Optional. Filter by items with theme songs.</param>
-        /// <param name="hasThemeVideo">Optional. Filter by items with theme videos.</param>
-        /// <param name="hasSubtitles">Optional. Filter by items with subtitles.</param>
-        /// <param name="hasSpecialFeatures">Optional. Filter by items with special features.</param>
-        /// <param name="hasTrailer">Optional. Filter by items with trailers.</param>
-        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
-        /// <param name="minIndexNumber">Optional. Filter by minimum index number.</param>
-        /// <param name="parentIndexNumber">Optional. Filter by parent index number.</param>
-        /// <param name="hasParentalRating">Optional. filter by items that have or do not have a parental rating.</param>
-        /// <param name="isHd">Optional. Filter by items that are HD or not.</param>
-        /// <param name="is4k">Optional. Filter by items that are 4K or not.</param>
-        /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
-        /// <param name="excludeLocationTypes">Optional. If specified, results will be excluded based on LocationType. This allows multiple, comma delimited.</param>
-        /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
-        /// <param name="isUnaired">Optional.  Filter by items that are unaired episodes or not.</param>
-        /// <param name="minCommunityRating">Optional. Filter by minimum community rating.</param>
-        /// <param name="minCriticRating">Optional. Filter by minimum critic rating.</param>
-        /// <param name="airedDuringSeason">Gets all episodes that aired during a season, including specials.</param>
-        /// <param name="minPremiereDate">Optional. The minimum premiere date.</param>
-        /// <param name="minDateLastSaved">Optional. The minimum last saved date.</param>
-        /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for user.</param>
-        /// <param name="maxPremiereDate">Optional. The maximum premiere date.</param>
-        /// <param name="hasOverview">Optional. Filter by items that have an overview or not.</param>
-        /// <param name="hasImdbId">Optional. Filter by items that have an imdb id or not.</param>
-        /// <param name="hasTmdbId">Optional. Filter by items that have a tmdb id or not.</param>
-        /// <param name="hasTvdbId">Optional. Filter by items that have a tvdb id or not.</param>
-        /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
         /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
-        /// <param name="isFavorite">Optional. Filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
-        /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
-        /// <param name="isPlayed">Optional. Filter by items that are played, or not.</param>
-        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
-        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
-        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
-        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
-        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
-        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
-        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
-        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
-        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
-        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
-        /// <param name="artists">Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimited.</param>
-        /// <param name="excludeArtistIds">Optional. If specified, results will be excluded based on artist id. This allows multiple, pipe delimited.</param>
-        /// <param name="artistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
-        /// <param name="albumArtistIds">Optional. If specified, results will be filtered based on album artist id. This allows multiple, pipe delimited.</param>
-        /// <param name="contributingArtistIds">Optional. If specified, results will be filtered based on contributing artist id. This allows multiple, pipe delimited.</param>
-        /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
-        /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
-        /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
-        /// <param name="videoTypes">Optional. Filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
         /// <param name="userId">User Id.</param>
-        /// <param name="minOfficialRating">Optional. Filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
-        /// <param name="isLocked">Optional. Filter by items that are locked.</param>
-        /// <param name="isPlaceholder">Optional. Filter by items that are placeholders.</param>
-        /// <param name="hasOfficialRating">Optional. Filter by items that have official ratings.</param>
-        /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
-        /// <param name="minWidth">Min width.</param>
-        /// <param name="minHeight">Min height.</param>
-        /// <param name="maxWidth">Max width.</param>
-        /// <param name="maxHeight">Max height.</param>
-        /// <param name="is3d">Optional. Filter by items that are 3D, or not.</param>
-        /// <param name="seriesStatus">Optional. Filter by Series Status. Allows multiple, comma delimited.</param>
-        /// <param name="nameStartsWithOrGreater">Optional. Filter by items whose name is sorted equally or greater than a given input string.</param>
-        /// <param name="nameStartsWith">Optional. Filter by items whose name is sorted equally than a given input string.</param>
-        /// <param name="nameLessThan">Optional. Filter by items whose name is equally or lesser than a given input string.</param>
         /// <param name="recursive">Search recursively.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
-        /// <param name="enableTotalRecordCount">Return total record count.</param>
         /// <response code="200">Year query returned.</response>
         /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxOfficialRating", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeSong", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasThemeVideo", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSubtitles", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasSpecialFeatures", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTrailer", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "adjacentTo", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minIndexNumber", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "parentIndexNumber", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasParentalRating", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isHd", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is4k", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "locationTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeLocationTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isMissing", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isUnaired", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCommunityRating", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minCriticRating", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "airedDuringSeason", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minPremiereDate", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSaved", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minDateLastSavedForUser", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxPremiereDate", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOverview", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasImdbId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTmdbId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasTvdbId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeItemIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "searchTerm", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "filters", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isFavorite", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlayed", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genres", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "genreIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "officialRatings", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "tags", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "years", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "person", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "personTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studios", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "studioIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artists", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "excludeArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "artistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "contributingArtistIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albums", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "albumIds", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "videoTypes", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isLocked", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isPlaceholder", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasOfficialRating", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "collapseBoxSetItems", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minWidth", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "minHeight", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxWidth", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "maxHeight", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "is3d", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesStatus", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWithOrGreater", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameStartsWith", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "nameLessThan", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<BaseItemDto>> GetYears(
-            [FromQuery] string maxOfficialRating,
-            [FromQuery] bool? hasThemeSong,
-            [FromQuery] bool? hasThemeVideo,
-            [FromQuery] bool? hasSubtitles,
-            [FromQuery] bool? hasSpecialFeatures,
-            [FromQuery] bool? hasTrailer,
-            [FromQuery] string adjacentTo,
-            [FromQuery] int? minIndexNumber,
-            [FromQuery] int? parentIndexNumber,
-            [FromQuery] bool? hasParentalRating,
-            [FromQuery] bool? isHd,
-            [FromQuery] bool? is4k,
-            [FromQuery] string locationTypes,
-            [FromQuery] string excludeLocationTypes,
-            [FromQuery] bool? isMissing,
-            [FromQuery] bool? isUnaired,
-            [FromQuery] double? minCommunityRating,
-            [FromQuery] double? minCriticRating,
-            [FromQuery] int? airedDuringSeason,
-            [FromQuery] DateTime? minPremiereDate,
-            [FromQuery] DateTime? minDateLastSaved,
-            [FromQuery] DateTime? minDateLastSavedForUser,
-            [FromQuery] DateTime? maxPremiereDate,
-            [FromQuery] bool? hasOverview,
-            [FromQuery] bool? hasImdbId,
-            [FromQuery] bool? hasTmdbId,
-            [FromQuery] bool? hasTvdbId,
-            [FromQuery] string excludeItemIds,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
             [FromQuery] string sortOrder,
             [FromQuery] string parentId,
             [FromQuery] string fields,
             [FromQuery] string excludeItemTypes,
             [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
-            [FromQuery] bool? isFavorite,
             [FromQuery] string mediaTypes,
-            [FromQuery] string imageTypes,
             [FromQuery] string sortBy,
-            [FromQuery] bool? isPlayed,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] string artists,
-            [FromQuery] string excludeArtistIds,
-            [FromQuery] string artistIds,
-            [FromQuery] string albumArtistIds,
-            [FromQuery] string contributingArtistIds,
-            [FromQuery] string albums,
-            [FromQuery] string albumIds,
-            [FromQuery] string ids,
-            [FromQuery] string videoTypes,
             [FromQuery] Guid userId,
-            [FromQuery] string minOfficialRating,
-            [FromQuery] bool? isLocked,
-            [FromQuery] bool? isPlaceholder,
-            [FromQuery] bool? hasOfficialRating,
-            [FromQuery] bool? collapseBoxSetItems,
-            [FromQuery] int? minWidth,
-            [FromQuery] int? minHeight,
-            [FromQuery] int? maxWidth,
-            [FromQuery] int? maxHeight,
-            [FromQuery] bool? is3d,
-            [FromQuery] string seriesStatus,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
             [FromQuery] bool recursive = true,
-            [FromQuery] bool? enableImages = true,
-            [FromQuery] bool enableTotalRecordCount = true)
+            [FromQuery] bool? enableImages = true)
         {
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)

From 7a32d03101410d00c79a4ad6ef34cae560d566c8 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 24 Jun 2020 14:43:28 -0600
Subject: [PATCH 259/463] remove unused parameters

---
 .../Controllers/ActivityLogController.cs      |  5 +--
 .../Controllers/DashboardController.cs        | 10 ++----
 Jellyfin.Api/Controllers/FilterController.cs  |  3 --
 .../Controllers/ItemRefreshController.cs      |  5 +--
 Jellyfin.Api/Controllers/LibraryController.cs | 23 +-----------
 .../Controllers/LibraryStructureController.cs |  4 +--
 .../Controllers/NotificationsController.cs    | 35 +++----------------
 Jellyfin.Api/Controllers/PluginsController.cs |  4 +--
 Jellyfin.Api/Controllers/TvShowsController.cs |  5 +--
 Jellyfin.Api/Controllers/UserController.cs    |  5 +--
 10 files changed, 14 insertions(+), 85 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index ec50fb022e..c287d1a773 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -35,17 +35,14 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
-        /// <param name="hasUserId">Optional. Only returns activities that have a user associated.</param>
         /// <response code="200">Activity log returned.</response>
         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
         [HttpGet("Entries")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] DateTime? minDate,
-            bool? hasUserId)
+            [FromQuery] DateTime? minDate)
         {
             var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
                 entries => entries.Where(entry => entry.DateCreated >= minDate));
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index aab920ff36..6cfee2463f 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -178,14 +178,13 @@ namespace Jellyfin.Api.Controllers
         [ApiExplorerSettings(IgnoreApi = true)]
         public ActionResult GetRobotsTxt()
         {
-            return GetWebClientResource("robots.txt", string.Empty);
+            return GetWebClientResource("robots.txt");
         }
 
         /// <summary>
         /// Gets a resource from the web client.
         /// </summary>
         /// <param name="resourceName">The resource name.</param>
-        /// <param name="v">The v.</param>
         /// <response code="200">Web client returned.</response>
         /// <response code="404">Server does not host a web client.</response>
         /// <returns>The resource.</returns>
@@ -193,10 +192,7 @@ namespace Jellyfin.Api.Controllers
         [ApiExplorerSettings(IgnoreApi = true)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")]
-        public ActionResult GetWebClientResource(
-            [FromRoute] string resourceName,
-            [FromQuery] string? v)
+        public ActionResult GetWebClientResource([FromRoute] string resourceName)
         {
             if (!_appConfig.HostWebClient() || WebClientUiPath == null)
             {
@@ -228,7 +224,7 @@ namespace Jellyfin.Api.Controllers
         [ApiExplorerSettings(IgnoreApi = true)]
         public ActionResult GetFavIcon()
         {
-            return GetWebClientResource("favicon.ico", string.Empty);
+            return GetWebClientResource("favicon.ico");
         }
 
         /// <summary>
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 0934a116a0..8a0a6ad866 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -125,7 +125,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">Optional. User id.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="mediaTypes">[Unused] Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="isAiring">Optional. Is item airing.</param>
         /// <param name="isMovie">Optional. Is item movie.</param>
         /// <param name="isSports">Optional. Is item sports.</param>
@@ -137,12 +136,10 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Query filters.</returns>
         [HttpGet("/Items/Filters2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryFilters> GetQueryFilters(
             [FromQuery] Guid? userId,
             [FromQuery] string? parentId,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? mediaTypes,
             [FromQuery] bool? isAiring,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSports,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index e6cdf4edbb..3801ce5b75 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -47,7 +47,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
         /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
         /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
-        /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
         /// <response code="204">Item metadata refresh queued.</response>
         /// <response code="404">Item to refresh not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -55,14 +54,12 @@ namespace Jellyfin.Api.Controllers
         [Description("Refreshes metadata for an item.")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")]
         public ActionResult Post(
             [FromRoute] Guid itemId,
             [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
             [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
             [FromQuery] bool replaceAllMetadata = false,
-            [FromQuery] bool replaceAllImages = false,
-            [FromQuery] bool recursive = false)
+            [FromQuery] bool replaceAllImages = false)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 9ad70024a2..8c76888152 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -120,22 +120,13 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets critic review for an item.
         /// </summary>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
-        /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <response code="200">Critic reviews returned.</response>
         /// <returns>The list of critic reviews.</returns>
         [HttpGet("/Items/{itemId}/CriticReviews")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Obsolete("This endpoint is obsolete.")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews(
-            [FromRoute] Guid itemId,
-            [FromQuery] int? startIndex,
-            [FromQuery] int? limit)
+        public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
         {
             return new QueryResult<BaseItemDto>();
         }
@@ -680,10 +671,6 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="itemId">The item id.</param>
         /// <param name="excludeArtistIds">Exclude artist ids.</param>
-        /// <param name="enableImages">(Unused) Optional. include image information in output.</param>
-        /// <param name="enableUserData">(Unused) Optional. include user data.</param>
-        /// <param name="imageTypeLimit">(Unused) Optional. the max number of images to return, per image type.</param>
-        /// <param name="enableImageTypes">(Unused) Optional. The image types to include in the output.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
@@ -695,18 +682,10 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Shows/{itemId}/Similar")]
         [HttpGet("/Movies/{itemId}/Similar")]
         [HttpGet("/Trailers/{itemId}/Similar")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
             [FromQuery] string excludeArtistIds,
-            [FromQuery] bool? enableImages,
-            [FromQuery] bool? enableUserData,
-            [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
             [FromQuery] string fields)
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 62c5474099..e4ac019c9a 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -50,13 +50,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets all virtual folders.
         /// </summary>
-        /// <param name="userId">The user id.</param>
         /// <response code="200">Virtual folders retrieved.</response>
         /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
+        public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
         {
             return _libraryManager.GetVirtualFolders(true);
         }
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index f226364894..cfa7545c96 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -36,23 +36,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a user's notifications.
         /// </summary>
-        /// <param name="userId">The user's ID.</param>
-        /// <param name="isRead">An optional filter by notification read state.</param>
-        /// <param name="startIndex">The optional index to start at. All notifications with a lower index will be omitted from the results.</param>
-        /// <param name="limit">An optional limit on the number of notifications returned.</param>
         /// <response code="200">Notifications returned.</response>
         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
         [HttpGet("{userId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
-        public ActionResult<NotificationResultDto> GetNotifications(
-            [FromRoute] string userId,
-            [FromQuery] bool? isRead,
-            [FromQuery] int? startIndex,
-            [FromQuery] int? limit)
+        public ActionResult<NotificationResultDto> GetNotifications()
         {
             return new NotificationResultDto();
         }
@@ -60,14 +48,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a user's notification summary.
         /// </summary>
-        /// <param name="userId">The user's ID.</param>
         /// <response code="200">Summary of user's notifications returned.</response>
         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
         [HttpGet("{userId}/Summary")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
-            [FromRoute] string userId)
+        public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
         {
             return new NotificationsSummaryDto();
         }
@@ -134,17 +119,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Sets notifications as read.
         /// </summary>
-        /// <param name="userId">The userID.</param>
-        /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
         /// <response code="204">Notifications set as read.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{userId}/Read")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
-        public ActionResult SetRead(
-            [FromRoute] string userId,
-            [FromQuery] string ids)
+        public ActionResult SetRead()
         {
             return NoContent();
         }
@@ -152,17 +131,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Sets notifications as unread.
         /// </summary>
-        /// <param name="userId">The userID.</param>
-        /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
         /// <response code="204">Notifications set as unread.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("{userId}/Unread")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
-        public ActionResult SetUnread(
-            [FromRoute] string userId,
-            [FromQuery] string ids)
+        public ActionResult SetUnread()
         {
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 979d401191..fd48983ea7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -42,13 +42,11 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a list of currently installed plugins.
         /// </summary>
-        /// <param name="isAppStoreEnabled">Optional. Unused.</param>
         /// <response code="200">Installed plugins returned.</response>
         /// <returns>List of currently installed plugins.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")]
-        public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
+        public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
         {
             return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
         }
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index bd950b39fd..6738dd8c85 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -185,12 +185,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
-        /// <param name="sortOrder">Optional. Sort order: Ascending,Descending.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
         [HttpGet("{seriesId}/Episodes")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "sortOrder", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
             [FromRoute] string seriesId,
             [FromQuery] Guid userId,
@@ -206,8 +204,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? imageTypeLimit,
             [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData,
-            [FromQuery] string? sortBy,
-            [FromQuery] SortOrder? sortOrder)
+            [FromQuery] string? sortBy)
         {
             var user = _userManager.GetUserById(userId);
 
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index c1f417df52..9f8d564a7d 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -68,17 +68,14 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
         /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
-        /// <param name="isGuest">Optional filter by IsGuest=true or false.</param>
         /// <response code="200">Users returned.</response>
         /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
         [HttpGet]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<UserDto>> GetUsers(
             [FromQuery] bool? isHidden,
-            [FromQuery] bool? isDisabled,
-            [FromQuery] bool? isGuest)
+            [FromQuery] bool? isDisabled)
         {
             var users = Get(isHidden, isDisabled, false, false);
             return Ok(users);

From dad6f12b1081ecf19204d039fbff7e45ad7dee2e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 25 Jun 2020 12:51:18 +0200
Subject: [PATCH 260/463] Move CollectionService to Jellyfin.Api

---
 .../Controllers/CollectionController.cs       | 108 ++++++++++++++++++
 MediaBrowser.Api/Movies/CollectionService.cs  | 105 -----------------
 2 files changed, 108 insertions(+), 105 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/CollectionController.cs
 delete mode 100644 MediaBrowser.Api/Movies/CollectionService.cs

diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
new file mode 100644
index 0000000000..d3f79afafc
--- /dev/null
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -0,0 +1,108 @@
+using System;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Collections;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The collection controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    [Route("/Collections")]
+    public class CollectionController : BaseJellyfinApiController
+    {
+        private readonly ICollectionManager _collectionManager;
+        private readonly IDtoService _dtoService;
+        private readonly IAuthorizationContext _authContext;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CollectionController"/> class.
+        /// </summary>
+        /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
+        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+        /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
+        public CollectionController(
+            ICollectionManager collectionManager,
+            IDtoService dtoService,
+            IAuthorizationContext authContext)
+        {
+            _collectionManager = collectionManager;
+            _dtoService = dtoService;
+            _authContext = authContext;
+        }
+
+        /// <summary>
+        /// Creates a new collection.
+        /// </summary>
+        /// <param name="name">The name of the collection.</param>
+        /// <param name="ids">Item Ids to add to the collection.</param>
+        /// <param name="isLocked">Whether or not to lock the new collection.</param>
+        /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
+        /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
+        [HttpPost]
+        public ActionResult<CollectionCreationResult> CreateCollection(
+            [FromQuery] string name,
+            [FromQuery] string ids,
+            [FromQuery] bool isLocked,
+            [FromQuery] Guid? parentId)
+        {
+            var userId = _authContext.GetAuthorizationInfo(Request).UserId;
+
+            var item = _collectionManager.CreateCollection(new CollectionCreationOptions
+            {
+                IsLocked = isLocked,
+                Name = name,
+                ParentId = parentId,
+                ItemIdList = RequestHelpers.Split(ids, ',', true),
+                UserIds = new[] { userId }
+            });
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
+
+            return new CollectionCreationResult
+            {
+                Id = dto.Id
+            };
+        }
+
+        /// <summary>
+        /// Adds items to a collection.
+        /// </summary>
+        /// <param name="collectionId">The collection id.</param>
+        /// <param name="itemIds">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 ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds)
+        {
+            _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Removes items from a collection.
+        /// </summary>
+        /// <param name="collectionId">The collection id.</param>
+        /// <param name="itemIds">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 ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds)
+        {
+            _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+            return NoContent();
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Movies/CollectionService.cs b/MediaBrowser.Api/Movies/CollectionService.cs
deleted file mode 100644
index 95a37dfc56..0000000000
--- a/MediaBrowser.Api/Movies/CollectionService.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Collections;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
-    [Route("/Collections", "POST", Summary = "Creates a new collection")]
-    public class CreateCollection : IReturn<CollectionCreationResult>
-    {
-        [ApiMember(Name = "IsLocked", Description = "Whether or not to lock the new collection.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool IsLocked { get; set; }
-
-        [ApiMember(Name = "Name", Description = "The name of the new collection.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Optional - create the collection within a specific folder", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "Ids", Description = "Item Ids to add to the collection", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
-    }
-
-    [Route("/Collections/{Id}/Items", "POST", Summary = "Adds items to a collection")]
-    public class AddToCollection : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Collections/{Id}/Items", "DELETE", Summary = "Removes items from a collection")]
-    public class RemoveFromCollection : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Authenticated]
-    public class CollectionService : BaseApiService
-    {
-        private readonly ICollectionManager _collectionManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        public CollectionService(
-            ILogger<CollectionService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ICollectionManager collectionManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _collectionManager = collectionManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Post(CreateCollection request)
-        {
-            var userId = _authContext.GetAuthorizationInfo(Request).UserId;
-
-            var parentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId);
-
-            var item = _collectionManager.CreateCollection(new CollectionCreationOptions
-            {
-                IsLocked = request.IsLocked,
-                Name = request.Name,
-                ParentId = parentId,
-                ItemIdList = SplitValue(request.Ids, ','),
-                UserIds = new[] { userId }
-
-            });
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
-
-            return new CollectionCreationResult
-            {
-                Id = dto.Id
-            };
-        }
-
-        public void Post(AddToCollection request)
-        {
-            _collectionManager.AddToCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
-        }
-
-        public void Delete(RemoveFromCollection request)
-        {
-            _collectionManager.RemoveFromCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
-        }
-    }
-}

From fa98013621071c8f30e86b7e5552004c00d72e39 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 25 Jun 2020 13:23:54 +0200
Subject: [PATCH 261/463] Move AlbumsService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AlbumsController.cs | 128 +++++++++++++
 Jellyfin.Api/Helpers/SimilarItemsHelper.cs   | 182 +++++++++++++++++++
 MediaBrowser.Api/Music/AlbumsService.cs      | 132 --------------
 3 files changed, 310 insertions(+), 132 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/AlbumsController.cs
 create mode 100644 Jellyfin.Api/Helpers/SimilarItemsHelper.cs
 delete mode 100644 MediaBrowser.Api/Music/AlbumsService.cs

diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
new file mode 100644
index 0000000000..88d3f643a1
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The albums controller.
+    /// </summary>
+    public class AlbumsController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AlbumsController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public AlbumsController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Finds albums similar to a given album.
+        /// </summary>
+        /// <param name="albumId">The album id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
+        [HttpGet("/Albums/{albumId}/Similar")]
+        public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
+            [FromRoute] string albumId,
+            [FromQuery] Guid userId,
+            [FromQuery] string excludeArtistIds,
+            [FromQuery] int? limit)
+        {
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            return SimilarItemsHelper.GetSimilarItemsResult(
+                dtoOptions,
+                _userManager,
+                _libraryManager,
+                _dtoService,
+                userId,
+                albumId,
+                excludeArtistIds,
+                limit,
+                new[] { typeof(MusicAlbum) },
+                GetAlbumSimilarityScore);
+        }
+
+        /// <summary>
+        /// Finds artists similar to a given artist.
+        /// </summary>
+        /// <param name="artistId">The artist id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
+        [HttpGet("/Artists/{artistId}/Similar")]
+        public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
+            [FromRoute] string artistId,
+            [FromQuery] Guid userId,
+            [FromQuery] string excludeArtistIds,
+            [FromQuery] int? limit)
+        {
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            return SimilarItemsHelper.GetSimilarItemsResult(
+                dtoOptions,
+                _userManager,
+                _libraryManager,
+                _dtoService,
+                userId,
+                artistId,
+                excludeArtistIds,
+                limit,
+                new[] { typeof(MusicArtist) },
+                SimilarItemsHelper.GetSimiliarityScore);
+        }
+
+        /// <summary>
+        /// Gets a similairty score of two albums.
+        /// </summary>
+        /// <param name="item1">The first item.</param>
+        /// <param name="item1People">The item1 people.</param>
+        /// <param name="allPeople">All people.</param>
+        /// <param name="item2">The second item.</param>
+        /// <returns>System.Int32.</returns>
+        private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
+        {
+            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
+
+            var album1 = (MusicAlbum)item1;
+            var album2 = (MusicAlbum)item2;
+
+            var artists1 = album1
+                .GetAllArtists()
+                .DistinctNames()
+                .ToList();
+
+            var artists2 = new HashSet<string>(
+                album2.GetAllArtists().DistinctNames(),
+                StringComparer.OrdinalIgnoreCase);
+
+            return points + artists1.Where(artists2.Contains).Sum(i => 5);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
new file mode 100644
index 0000000000..751e3c4815
--- /dev/null
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// The similar items helper class.
+    /// </summary>
+    public static class SimilarItemsHelper
+    {
+        internal static QueryResult<BaseItemDto> GetSimilarItemsResult(
+            DtoOptions dtoOptions,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            Guid userId,
+            string id,
+            string excludeArtistIds,
+            int? limit,
+            Type[] includeTypes,
+            Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
+        {
+            var user = !userId.Equals(Guid.Empty) ? userManager.GetUserById(userId) : null;
+
+            var item = string.IsNullOrEmpty(id) ?
+                (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
+                libraryManager.RootFolder) : libraryManager.GetItemById(id);
+
+            var query = new InternalItemsQuery(user)
+            {
+                IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
+                Recursive = true,
+                DtoOptions = dtoOptions
+            };
+
+            query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+
+            var inputItems = libraryManager.GetItemList(query);
+
+            var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
+                .ToList();
+
+            var returnItems = items;
+
+            if (limit.HasValue)
+            {
+                returnItems = returnItems.Take(limit.Value).ToList();
+            }
+
+            var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos,
+                TotalRecordCount = items.Count
+            };
+        }
+
+        /// <summary>
+        /// Gets the similaritems.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="inputItems">The input items.</param>
+        /// <param name="getSimilarityScore">The get similarity score.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private static IEnumerable<BaseItem> GetSimilaritems(
+            BaseItem item,
+            ILibraryManager libraryManager,
+            IEnumerable<BaseItem> inputItems,
+            Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
+        {
+            var itemId = item.Id;
+            inputItems = inputItems.Where(i => i.Id != itemId);
+            var itemPeople = libraryManager.GetPeople(item);
+            var allPeople = libraryManager.GetPeople(new InternalPeopleQuery
+            {
+                AppearsInItemId = item.Id
+            });
+
+            return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, itemPeople, allPeople, i)))
+                .Where(i => i.Item2 > 2)
+                .OrderByDescending(i => i.Item2)
+                .Select(i => i.Item1);
+        }
+
+        private static IEnumerable<string> GetTags(BaseItem item)
+        {
+            return item.Tags;
+        }
+
+        /// <summary>
+        /// Gets the similiarity score.
+        /// </summary>
+        /// <param name="item1">The item1.</param>
+        /// <param name="item1People">The item1 people.</param>
+        /// <param name="allPeople">All people.</param>
+        /// <param name="item2">The item2.</param>
+        /// <returns>System.Int32.</returns>
+        internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
+        {
+            var points = 0;
+
+            if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
+            {
+                points += 10;
+            }
+
+            // Find common genres
+            points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
+
+            // Find common tags
+            points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
+
+            // Find common studios
+            points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
+
+            var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
+                .Select(i => i.Name)
+                .Where(i => !string.IsNullOrWhiteSpace(i))
+                .DistinctNames()
+                .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+            points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
+            {
+                if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 5;
+                }
+
+                if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+
+                if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+
+                if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+
+                if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 2;
+                }
+
+                return 1;
+            });
+
+            if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue)
+            {
+                var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value);
+
+                // Add if they came out within the same decade
+                if (diff < 10)
+                {
+                    points += 2;
+                }
+
+                // And more if within five years
+                if (diff < 5)
+                {
+                    points += 2;
+                }
+            }
+
+            return points;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs
deleted file mode 100644
index f257d10141..0000000000
--- a/MediaBrowser.Api/Music/AlbumsService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
-    [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    public class GetSimilarAlbums : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    public class GetSimilarArtists : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Authenticated]
-    public class AlbumsService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _user data repository
-        /// </summary>
-        private readonly IUserDataManager _userDataRepository;
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly IItemRepository _itemRepo;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        public AlbumsService(
-            ILogger<AlbumsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserDataManager userDataRepository,
-            ILibraryManager libraryManager,
-            IItemRepository itemRepo,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userDataRepository = userDataRepository;
-            _libraryManager = libraryManager;
-            _itemRepo = itemRepo;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Get(GetSimilarArtists request)
-        {
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions, 
-                _userManager,
-                _itemRepo,
-                _libraryManager,
-                _userDataRepository,
-                _dtoService,
-                request, new[] { typeof(MusicArtist) },
-                SimilarItemsHelper.GetSimiliarityScore);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSimilarAlbums request)
-        {
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions, 
-                _userManager,
-                _itemRepo,
-                _libraryManager,
-                _userDataRepository,
-                _dtoService,
-                request, new[] { typeof(MusicAlbum) },
-                GetAlbumSimilarityScore);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the album similarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item1People">The item1 people.</param>
-        /// <param name="allPeople">All people.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
-        {
-            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
-
-            var album1 = (MusicAlbum)item1;
-            var album2 = (MusicAlbum)item2;
-
-            var artists1 = album1
-                .GetAllArtists()
-                .DistinctNames()
-                .ToList();
-
-            var artists2 = new HashSet<string>(
-                album2.GetAllArtists().DistinctNames(),
-                StringComparer.OrdinalIgnoreCase);
-
-            return points + artists1.Where(artists2.Contains).Sum(i => 5);
-        }
-    }
-}

From 2cc5b1ab94369cd88be826f7b44576e67293eb83 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 25 Jun 2020 17:07:12 +0200
Subject: [PATCH 262/463] Add response code docs

---
 Jellyfin.Api/Controllers/AlbumsController.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 88d3f643a1..622123873d 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -45,8 +46,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <response code="200">Similar albums returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
         [HttpGet("/Albums/{albumId}/Similar")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
             [FromRoute] string albumId,
             [FromQuery] Guid userId,
@@ -75,8 +78,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <response code="200">Similar artists returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
         [HttpGet("/Artists/{artistId}/Similar")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
             [FromRoute] string artistId,
             [FromQuery] Guid userId,

From 53f4a8ce58282902d7cae34f07488ebd71447f2d Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 25 Jun 2020 17:15:08 +0200
Subject: [PATCH 263/463] Add missing response code documentation

---
 Jellyfin.Api/Controllers/CollectionController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index d3f79afafc..29db0b1782 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -46,8 +46,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ids">Item Ids to add to the collection.</param>
         /// <param name="isLocked">Whether or not to lock the new collection.</param>
         /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
+        /// <response code="200">Collection created.</response>
         /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<CollectionCreationResult> CreateCollection(
             [FromQuery] string name,
             [FromQuery] string ids,

From 835bda7be334915a013a6e0feef3a43ce0ed2ee6 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 13:01:41 -0600
Subject: [PATCH 264/463] Add missing route

---
 Jellyfin.Api/Controllers/LibraryController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 9ad70024a2..b822b39910 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -281,6 +281,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs and videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
+        [HttpGet("/Items/{itemId}/ThemeMedia")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
             [FromRoute] Guid itemId,

From 0179293c24b8fe4000612b5538f14082719bf8f4 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 16:23:31 -0600
Subject: [PATCH 265/463] fix build

---
 .../Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs    | 2 +-
 Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
index a86815b81c..92be15b8a6 100644
--- a/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
+++ b/Jellyfin.Api/Models/EnvironmentDtos/DefaultDirectoryBrowserInfoDto.cs
@@ -8,6 +8,6 @@ namespace Jellyfin.Api.Models.EnvironmentDtos
         /// <summary>
         /// Gets or sets the path.
         /// </summary>
-        public string Path { get; set; }
+        public string? Path { get; set; }
     }
 }
diff --git a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
index 60c82e166b..418c11c2d0 100644
--- a/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
+++ b/Jellyfin.Api/Models/EnvironmentDtos/ValidatePathDto.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Models.EnvironmentDtos
         /// <summary>
         /// Gets or sets the path.
         /// </summary>
-        public string Path { get; set; }
+        public string? Path { get; set; }
 
         /// <summary>
         /// Gets or sets is path file.

From 54d666d7c9357901ea76fd405c32c5fcf8ec4431 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 16:46:09 -0600
Subject: [PATCH 266/463] move UserViewsService.cs to Jellyfin.Api

---
 .../Controllers/UserViewsController.cs        | 148 ++++++++++++++++++
 .../UserViewDtos/SpecialViewOptionDto.cs      |  18 +++
 .../UserLibrary/UserViewsService.cs           | 146 -----------------
 3 files changed, 166 insertions(+), 146 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/UserViewsController.cs
 create mode 100644 Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/UserViewsService.cs

diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
new file mode 100644
index 0000000000..38bf940876
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.UserViewDtos;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// User views controller.
+    /// </summary>
+    public class UserViewsController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly IUserViewManager _userViewManager;
+        private readonly IDtoService _dtoService;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserViewsController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public UserViewsController(
+            IUserManager userManager,
+            IUserViewManager userViewManager,
+            IDtoService dtoService,
+            IAuthorizationContext authContext,
+            ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _userViewManager = userViewManager;
+            _dtoService = dtoService;
+            _authContext = authContext;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Get user views.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
+        /// <param name="includeHidden">Whether or not to include hidden content.</param>
+        /// <param name="presetViews">Preset views.</param>
+        /// <response code="200">User views returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
+        [HttpGet("/Users/{userId}/Views")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
+            [FromRoute] Guid userId,
+            [FromQuery] bool? includeExternalContent,
+            [FromQuery] bool includeHidden,
+            [FromQuery] string presetViews)
+        {
+            var query = new UserViewQuery
+            {
+                UserId = userId,
+                IncludeHidden = includeHidden
+            };
+
+            if (includeExternalContent.HasValue)
+            {
+                query.IncludeExternalContent = includeExternalContent.Value;
+            }
+
+            if (!string.IsNullOrWhiteSpace(presetViews))
+            {
+                query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
+            }
+
+            var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
+            if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
+            }
+
+            var folders = _userViewManager.GetUserViews(query);
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var fields = dtoOptions.Fields.ToList();
+
+            fields.Add(ItemFields.PrimaryImageAspectRatio);
+            fields.Add(ItemFields.DisplayPreferencesId);
+            fields.Remove(ItemFields.BasicSyncInfo);
+            dtoOptions.Fields = fields.ToArray();
+
+            var user = _userManager.GetUserById(userId);
+
+            var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
+                .ToArray();
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos,
+                TotalRecordCount = dtos.Length
+            };
+        }
+
+        /// <summary>
+        /// Get user view grouping options.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <response code="200">User view grouping options returned.</response>
+        /// <response code="404">User not found.</response>
+        /// <returns>
+        /// An <see cref="OkResult"/> containing the user view grouping options
+        /// or a <see cref="NotFoundResult"/> if user not found.
+        /// </returns>
+        [HttpGet("/Users/{userId}/GroupingOptions")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute] Guid userId)
+        {
+            var user = _userManager.GetUserById(userId);
+            if (user == null)
+            {
+                return NotFound();
+            }
+
+            return Ok(_libraryManager.GetUserRootFolder()
+                .GetChildren(user, true)
+                .OfType<Folder>()
+                .Where(UserView.IsEligibleForGrouping)
+                .Select(i => new SpecialViewOptionDto
+                {
+                    Name = i.Name,
+                    Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
+                })
+                .OrderBy(i => i.Name));
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
new file mode 100644
index 0000000000..84b6b0958c
--- /dev/null
+++ b/Jellyfin.Api/Models/UserViewDtos/SpecialViewOptionDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.UserViewDtos
+{
+    /// <summary>
+    /// Special view option dto.
+    /// </summary>
+    public class SpecialViewOptionDto
+    {
+        /// <summary>
+        /// Gets or sets view option name.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets view option id.
+        /// </summary>
+        public string? Id { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs
deleted file mode 100644
index 0fffb06223..0000000000
--- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Library;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    [Route("/Users/{UserId}/Views", "GET")]
-    public class GetUserViews : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "IncludeExternalContent", Description = "Whether or not to include external views such as channels or live tv", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? IncludeExternalContent { get; set; }
-        public bool IncludeHidden { get; set; }
-
-        public string PresetViews { get; set; }
-    }
-
-    [Route("/Users/{UserId}/GroupingOptions", "GET")]
-    public class GetGroupingOptions : IReturn<SpecialViewOption[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    public class UserViewsService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserViewManager _userViewManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly ILibraryManager _libraryManager;
-
-        public UserViewsService(
-            ILogger<UserViewsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserViewManager userViewManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userViewManager = userViewManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetUserViews request)
-        {
-            var query = new UserViewQuery
-            {
-                UserId = request.UserId
-            };
-
-            if (request.IncludeExternalContent.HasValue)
-            {
-                query.IncludeExternalContent = request.IncludeExternalContent.Value;
-            }
-            query.IncludeHidden = request.IncludeHidden;
-
-            if (!string.IsNullOrWhiteSpace(request.PresetViews))
-            {
-                query.PresetViews = request.PresetViews.Split(',');
-            }
-
-            var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
-            if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
-            }
-
-            var folders = _userViewManager.GetUserViews(query);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var fields = dtoOptions.Fields.ToList();
-
-            fields.Add(ItemFields.PrimaryImageAspectRatio);
-            fields.Add(ItemFields.DisplayPreferencesId);
-            fields.Remove(ItemFields.BasicSyncInfo);
-            dtoOptions.Fields = fields.ToArray();
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
-                .ToArray();
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = dtos.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetGroupingOptions request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var list = _libraryManager.GetUserRootFolder()
-                .GetChildren(user, true)
-                .OfType<Folder>()
-                .Where(UserView.IsEligibleForGrouping)
-                .Select(i => new SpecialViewOption
-                {
-                    Name = i.Name,
-                    Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
-
-                })
-            .OrderBy(i => i.Name)
-            .ToArray();
-
-            return ToOptimizedResult(list);
-        }
-    }
-
-    class SpecialViewOption
-    {
-        public string Name { get; set; }
-        public string Id { get; set; }
-    }
-}

From 8074c47d294fffabe0d0a423ae98ab6d4917d2b9 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 17:28:12 -0600
Subject: [PATCH 267/463] Move UserLibraryService.cs to Jellyfin.Api

---
 .../Controllers/UserLibraryController.cs      | 391 ++++++++++++
 .../UserLibrary/UserLibraryService.cs         | 575 ------------------
 2 files changed, 391 insertions(+), 575 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/UserLibraryController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/UserLibraryService.cs

diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
new file mode 100644
index 0000000000..597e704693
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// User library controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class UserLibraryController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataRepository;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IUserViewManager _userViewManager;
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        public UserLibraryController(
+            IUserManager userManager,
+            IUserDataManager userDataRepository,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            IUserViewManager userViewManager,
+            IFileSystem fileSystem)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _userViewManager = userViewManager;
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Gets an item from a user's library.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Item returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the d item.</returns>
+        [HttpGet("/Users/{userId}/Items/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty)
+                ? _libraryManager.GetUserRootFolder()
+                : _libraryManager.GetItemById(itemId);
+
+            await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+        }
+
+        /// <summary>
+        /// Gets the root folder from a user's library.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <response code="200">Root folder returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
+        [HttpGet("/Users/{userId}/Items/Root")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BaseItemDto> GetRootFolder([FromRoute] Guid userId)
+        {
+            var user = _userManager.GetUserById(userId);
+            var item = _libraryManager.GetUserRootFolder();
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+        }
+
+        /// <summary>
+        /// Gets intros to play before the main media item plays.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Intros returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
+        [HttpGet("/Users/{userId}/Items/{itemId}/Intros")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty)
+                ? _libraryManager.GetUserRootFolder()
+                : _libraryManager.GetItemById(itemId);
+
+            var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos,
+                TotalRecordCount = dtos.Length
+            };
+        }
+
+        /// <summary>
+        /// Marks an item as a favorite.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Item marked as favorite.</response>
+        /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpPost("/Users/{userId}/FavoriteItems/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            return MarkFavorite(userId, itemId, true);
+        }
+
+        /// <summary>
+        /// Unmarks item as a favorite.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Item unmarked as favorite.</response>
+        /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpDelete("/Users/{userId}/FavoriteItems/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            return MarkFavorite(userId, itemId, false);
+        }
+
+        /// <summary>
+        /// Deletes a user's saved personal rating for an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Personal rating removed.</response>
+        /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpDelete("/Users/{userId}/Items/{itemId}/Rating")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            return UpdateUserItemRatingInternal(userId, itemId, null);
+        }
+
+        /// <summary>
+        /// Updates a user's rating for an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
+        /// <response code="200">Item rating updated.</response>
+        /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpPost("/Users/{userId}/Items/{itemId}/Rating")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool likes)
+        {
+            return UpdateUserItemRatingInternal(userId, itemId, likes);
+        }
+
+        /// <summary>
+        /// Gets local trailers for an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
+        /// <returns>The items local trailers.</returns>
+        [HttpGet("/Users/{userId}/Items/{itemId}/LocalTrailers")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty)
+                ? _libraryManager.GetUserRootFolder()
+                : _libraryManager.GetItemById(itemId);
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+            var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
+                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+                .ToArray();
+
+            if (item is IHasTrailers hasTrailers)
+            {
+                var trailers = hasTrailers.GetTrailers();
+                var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
+                var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
+                dtosExtras.CopyTo(allTrailers, 0);
+                dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
+                return allTrailers;
+            }
+
+            return dtosExtras;
+        }
+
+        /// <summary>
+        /// Gets special features for an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Special features returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
+        [HttpGet("/Users/{userId}/Items/{itemId}/SpecialFeatures")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty)
+                ? _libraryManager.GetUserRootFolder()
+                : _libraryManager.GetItemById(itemId);
+
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            return Ok(item
+                .GetExtras(BaseItem.DisplayExtraTypes)
+                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
+        }
+
+        /// <summary>
+        /// Gets latest media.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+        /// <param name="isPlayed">Filter by items that are played, or not.</param>
+        /// <param name="enableImages">Optional. include image information in output.</param>
+        /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="enableUserData">Optional. include user data.</param>
+        /// <param name="limit">Return item limit.</param>
+        /// <param name="groupItems">Whether or not to group items into a parent container.</param>
+        /// <response code="200">Latest media returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
+        [HttpGet("/Users/{userId}/Items/Latest")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
+            [FromRoute] Guid userId,
+            [FromQuery] Guid parentId,
+            [FromQuery] string fields,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] bool? isPlayed,
+            [FromQuery] bool? enableImages,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int limit = 20,
+            [FromQuery] bool groupItems = true)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            if (!isPlayed.HasValue)
+            {
+                if (user.HidePlayedInLatest)
+                {
+                    isPlayed = false;
+                }
+            }
+
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            var list = _userViewManager.GetLatestItems(
+                new LatestItemsQuery
+                {
+                    GroupItems = groupItems,
+                    IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                    IsPlayed = isPlayed,
+                    Limit = limit,
+                    ParentId = parentId,
+                    UserId = userId,
+                }, dtoOptions);
+
+            var dtos = list.Select(i =>
+            {
+                var item = i.Item2[0];
+                var childCount = 0;
+
+                if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
+                {
+                    item = i.Item1;
+                    childCount = i.Item2.Count;
+                }
+
+                var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
+
+                dto.ChildCount = childCount;
+
+                return dto;
+            });
+
+            return Ok(dtos);
+        }
+
+        private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+        {
+            if (item is Person)
+            {
+                var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+                var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+
+                if (!hasMetdata)
+                {
+                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                    {
+                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+                        ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                        ForceSave = performFullRefresh
+                    };
+
+                    await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Marks the favorite.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
+        private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+
+            // Get the user data for this item
+            var data = _userDataRepository.GetUserData(user, item);
+
+            // Set favorite status
+            data.IsFavorite = isFavorite;
+
+            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+
+            return _userDataRepository.GetUserDataDto(item, user);
+        }
+
+        /// <summary>
+        /// Updates the user item rating.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="likes">if set to <c>true</c> [likes].</param>
+        private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes)
+        {
+            var user = _userManager.GetUserById(userId);
+
+            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+
+            // Get the user data for this item
+            var data = _userDataRepository.GetUserData(user, item);
+
+            data.Likes = likes;
+
+            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+
+            return _userDataRepository.GetUserDataDto(item, user);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
deleted file mode 100644
index f758528859..0000000000
--- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
+++ /dev/null
@@ -1,575 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetItem
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")]
-    public class GetItem : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetItem
-    /// </summary>
-    [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")]
-    public class GetRootFolder : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetIntros
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")]
-    public class GetIntros : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the item id.
-        /// </summary>
-        /// <value>The item id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class MarkFavoriteItem
-    /// </summary>
-    [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")]
-    public class MarkFavoriteItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UnmarkFavoriteItem
-    /// </summary>
-    [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")]
-    public class UnmarkFavoriteItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class ClearUserItemRating
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")]
-    public class DeleteUserItemRating : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserItemRating
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")]
-    public class UpdateUserItemRating : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
-        /// </summary>
-        /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "Likes", Description = "Whether the user likes the item or not. true/false", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool Likes { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetLocalTrailers
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")]
-    public class GetLocalTrailers : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetSpecialFeatures
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")]
-    public class GetSpecialFeatures : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")]
-    public class GetLatestMedia : IReturn<BaseItemDto[]>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int Limit { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid ParentId { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFolder { get; set; }
-
-        [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlayed { get; set; }
-
-        [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool GroupItems { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public GetLatestMedia()
-        {
-            Limit = 20;
-            GroupItems = true;
-        }
-    }
-
-    /// <summary>
-    /// Class UserLibraryService
-    /// </summary>
-    [Authenticated]
-    public class UserLibraryService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserDataManager _userDataRepository;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IUserViewManager _userViewManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IAuthorizationContext _authContext;
-
-        public UserLibraryService(
-            ILogger<UserLibraryService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IUserViewManager userViewManager,
-            IFileSystem fileSystem,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _userDataRepository = userDataRepository;
-            _dtoService = dtoService;
-            _userViewManager = userViewManager;
-            _fileSystem = fileSystem;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSpecialFeatures request)
-        {
-            var result = GetAsync(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetLatestMedia request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            if (!request.IsPlayed.HasValue)
-            {
-                if (user.HidePlayedInLatest)
-                {
-                    request.IsPlayed = false;
-                }
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var list = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                GroupItems = request.GroupItems,
-                IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
-                IsPlayed = request.IsPlayed,
-                Limit = request.Limit,
-                ParentId = request.ParentId,
-                UserId = request.UserId,
-            }, dtoOptions);
-
-            var dtos = list.Select(i =>
-            {
-                var item = i.Item2[0];
-                var childCount = 0;
-
-                if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
-                {
-                    item = i.Item1;
-                    childCount = i.Item2.Count;
-                }
-
-                var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-                dto.ChildCount = childCount;
-
-                return dto;
-            });
-
-            return ToOptimizedResult(dtos.ToArray());
-        }
-
-        private BaseItemDto[] GetAsync(GetSpecialFeatures request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                _libraryManager.GetUserRootFolder() :
-                _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = item
-                .GetExtras(BaseItem.DisplayExtraTypes)
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
-
-            return dtos.ToArray();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetLocalTrailers request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            if (item is IHasTrailers hasTrailers)
-            {
-                var trailers = hasTrailers.GetTrailers();
-                var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
-                var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
-                dtosExtras.CopyTo(allTrailers, 0);
-                dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
-                return ToOptimizedResult(allTrailers);
-            }
-
-            return ToOptimizedResult(dtosExtras);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetItem request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
-        {
-            if (item is Person)
-            {
-                var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
-                var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
-
-                if (!hasMetdata)
-                {
-                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                    {
-                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                        ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                        ForceSave = performFullRefresh
-                    };
-
-                    await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRootFolder request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = _libraryManager.GetUserRootFolder();
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetIntros request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = dtos.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(MarkFavoriteItem request)
-        {
-            var dto = MarkFavorite(request.UserId, request.Id, true);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(UnmarkFavoriteItem request)
-        {
-            var dto = MarkFavorite(request.UserId, request.Id, false);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Marks the favorite.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
-        private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
-        {
-            var user = _userManager.GetUserById(userId);
-
-            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
-            // Get the user data for this item
-            var data = _userDataRepository.GetUserData(user, item);
-
-            // Set favorite status
-            data.IsFavorite = isFavorite;
-
-            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(DeleteUserItemRating request)
-        {
-            var dto = UpdateUserItemRating(request.UserId, request.Id, null);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(UpdateUserItemRating request)
-        {
-            var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Updates the user item rating.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="likes">if set to <c>true</c> [likes].</param>
-        private UserItemDataDto UpdateUserItemRating(Guid userId, Guid itemId, bool? likes)
-        {
-            var user = _userManager.GetUserById(userId);
-
-            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
-            // Get the user data for this item
-            var data = _userDataRepository.GetUserData(user, item);
-
-            data.Likes = likes;
-
-            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-    }
-}

From 8d7b39a36e388bda2bc09f3896714bdefa2866b7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 17:44:11 -0600
Subject: [PATCH 268/463] fix endpoint order

---
 Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index e4ecd343c5..cfbabf7954 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -195,7 +195,7 @@ namespace Jellyfin.Server.Extensions
 
                 // Order actions by route path, then by http method.
                 c.OrderActionsBy(description =>
-                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}");
+                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 
                 // Use method name as operationId
                 c.CustomOperationIds(description =>

From 325808d271a1e53bcf3b15f17b0111c0ce306698 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 25 Jun 2020 18:22:55 -0600
Subject: [PATCH 269/463] Move StudiosService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/StudiosController.cs | 277 ++++++++++++++++++
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  14 +
 .../UserLibrary/StudiosService.cs             | 132 ---------
 3 files changed, 291 insertions(+), 132 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/StudiosController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/StudiosService.cs

diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
new file mode 100644
index 0000000000..76cf2febfe
--- /dev/null
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Studios controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class StudiosController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StudiosController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public StudiosController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets all studios from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">Optional. Search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Total record count.</param>
+        /// <response code="200">Studios returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the studios.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetStudios(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+            var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+            var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = excludeItemTypesArr,
+                IncludeItemTypes = includeItemTypesArr,
+                MediaTypes = mediaTypesArr,
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|').Select(i =>
+                {
+                    try
+                    {
+                        return _libraryManager.GetStudio(i);
+                    }
+                    catch
+                    {
+                        return null;
+                    }
+                }).Where(i => i != null).Select(i => i!.Id)
+                    .ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = new QueryResult<(BaseItem, ItemCounts)>();
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, itemCounts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = itemCounts.ItemCount;
+                    dto.ProgramCount = itemCounts.ProgramCount;
+                    dto.SeriesCount = itemCounts.SeriesCount;
+                    dto.EpisodeCount = itemCounts.EpisodeCount;
+                    dto.MovieCount = itemCounts.MovieCount;
+                    dto.TrailerCount = itemCounts.TrailerCount;
+                    dto.AlbumCount = itemCounts.AlbumCount;
+                    dto.SongCount = itemCounts.SongCount;
+                    dto.ArtistCount = itemCounts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets a studio by name.
+        /// </summary>
+        /// <param name="name">Studio name.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Studio returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the studio.</returns>
+        [HttpGet("{name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid userId)
+        {
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            var item = _libraryManager.GetStudio(name);
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 446ee716aa..eec9232928 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -3,6 +3,7 @@ using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
@@ -91,5 +92,18 @@ namespace Jellyfin.Api.Helpers
                 .Select(i => new Guid(i))
                 .ToArray();
         }
+
+        /// <summary>
+        /// Gets the filters.
+        /// </summary>
+        /// <param name="filters">The filter string.</param>
+        /// <returns>IEnumerable{ItemFilter}.</returns>
+        internal static ItemFilter[] GetFilters(string filters)
+        {
+            return string.IsNullOrEmpty(filters)
+                ? Array.Empty<ItemFilter>()
+                : Split(filters, ',', true)
+                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
+        }
     }
 }
diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs
deleted file mode 100644
index 683ce5d09d..0000000000
--- a/MediaBrowser.Api/UserLibrary/StudiosService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetStudios
-    /// </summary>
-    [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")]
-    public class GetStudios : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetStudio
-    /// </summary>
-    [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")]
-    public class GetStudio : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class StudiosService
-    /// </summary>
-    [Authenticated]
-    public class StudiosService : BaseItemsByNameService<Studio>
-    {
-        public StudiosService(
-            ILogger<StudiosService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetStudio request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetStudio request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetStudio(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetStudios request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return LibraryManager.GetStudios(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

From 778634b41b7337ab676bf2331939ea11106af20e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 26 Jun 2020 13:42:21 +0200
Subject: [PATCH 270/463] Move InstantMixService to Jellyfin.Api

---
 .../Controllers/InstantMixController.cs       | 299 ++++++++++++++++++
 Jellyfin.Api/Controllers/LibraryController.cs |   1 +
 MediaBrowser.Api/Music/InstantMixService.cs   | 197 ------------
 3 files changed, 300 insertions(+), 197 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/InstantMixController.cs
 delete mode 100644 MediaBrowser.Api/Music/InstantMixService.cs

diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
new file mode 100644
index 0000000000..6b4670d6cd
--- /dev/null
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The instant mix controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class InstantMixController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMusicManager _musicManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="InstantMixController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public InstantMixController(
+            IUserManager userManager,
+            IDtoService dtoService,
+            IMusicManager musicManager,
+            ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _dtoService = dtoService;
+            _musicManager = musicManager;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/Songs/{id}/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var item = _libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/Albums/{id}/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var album = _libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/Playlists/{id}/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var playlist = (Playlist)_libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="name">The genre name.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/MusicGenres/{name}/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
+            [FromRoute] string name,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/Artists/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var item = _libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/MusicGenres/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var item = _libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given song.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("/Items/{id}/InstantMix")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
+            [FromRoute] Guid id,
+            [FromQuery] Guid userId,
+            [FromQuery] int? limit,
+            [FromQuery] string fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes)
+        {
+            var item = _libraryManager.GetItemById(id);
+            var user = _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+            return GetResult(items, user, limit, dtoOptions);
+        }
+
+        private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User user, int? limit, DtoOptions dtoOptions)
+        {
+            var list = items;
+
+            var result = new QueryResult<BaseItemDto>
+            {
+                TotalRecordCount = list.Count
+            };
+
+            if (limit.HasValue)
+            {
+                list = list.Take(limit.Value).ToList();
+            }
+
+            var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
+
+            result.Items = returnList;
+
+            return result;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 9ad70024a2..640308d4aa 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -281,6 +281,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs and videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
+        [HttpGet("ThemeMedia")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
             [FromRoute] Guid itemId,
diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs
deleted file mode 100644
index 7d10c94271..0000000000
--- a/MediaBrowser.Api/Music/InstantMixService.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
-    [Route("/Songs/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given song")]
-    public class GetInstantMixFromSong : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Albums/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given album")]
-    public class GetInstantMixFromAlbum : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Playlists/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given playlist")]
-    public class GetInstantMixFromPlaylist : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/MusicGenres/{Name}/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
-    public class GetInstantMixFromMusicGenre : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    [Route("/Artists/InstantMix", "GET", Summary = "Creates an instant playlist based on a given artist")]
-    public class GetInstantMixFromArtistId : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Id", Description = "The artist Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
-    public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given item")]
-    public class GetInstantMixFromItem : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Authenticated]
-    public class InstantMixService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-
-        private readonly IDtoService _dtoService;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMusicManager _musicManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public InstantMixService(
-            ILogger<InstantMixService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IDtoService dtoService,
-            IMusicManager musicManager,
-            ILibraryManager libraryManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _musicManager = musicManager;
-            _libraryManager = libraryManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetInstantMixFromItem request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromArtistId request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromMusicGenreId request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromSong request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromAlbum request)
-        {
-            var album = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromPlaylist request)
-        {
-            var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromMusicGenre request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        private object GetResult(List<BaseItem> items, User user, BaseGetSimilarItems request, DtoOptions dtoOptions)
-        {
-            var list = items;
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = list.Count
-            };
-
-            if (request.Limit.HasValue)
-            {
-                list = list.Take(request.Limit.Value).ToList();
-            }
-
-            var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
-            result.Items = returnList;
-
-            return result;
-        }
-
-    }
-}

From fb9654e783a153871d484fcdb65cac905a1729b2 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 26 Jun 2020 13:43:31 +0200
Subject: [PATCH 271/463] Correct Library routing

---
 Jellyfin.Api/Controllers/LibraryController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 640308d4aa..f525076fb0 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs and videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
-        [HttpGet("ThemeMedia")]
+        [HttpGet("/Items/{itemId}/ThemeMedia")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
             [FromRoute] Guid itemId,

From 1e80be30a9a429627a20e894cb2bdf5349ca7ff8 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 26 Jun 2020 17:45:07 +0200
Subject: [PATCH 272/463] Add response code documentation

---
 Jellyfin.Api/Controllers/InstantMixController.cs | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 6b4670d6cd..f1ff770a40 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -56,8 +57,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/Songs/{id}/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
@@ -89,8 +92,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/Albums/{id}/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
@@ -122,8 +127,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/Playlists/{id}/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
@@ -155,8 +162,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/MusicGenres/{name}/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
             [FromRoute] string name,
             [FromQuery] Guid userId,
@@ -187,8 +196,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/Artists/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
@@ -220,8 +231,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/MusicGenres/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
@@ -253,8 +266,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("/Items/{id}/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
             [FromRoute] Guid id,
             [FromQuery] Guid userId,

From f45d44f32150b53231af9651021d1dee690775f1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 26 Jun 2020 21:04:02 -0600
Subject: [PATCH 273/463] Move PlaystateService.cs to Jellyfin.Api

---
 .../Controllers/PlaystateController.cs        | 372 ++++++++++++++
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  | 354 ++++++++++++++
 .../Models/PlaybackDtos/TranscodingJobDto.cs  | 256 ++++++++++
 .../PlaybackDtos/TranscodingThrottler.cs      | 212 ++++++++
 .../UserLibrary/PlaystateService.cs           | 456 ------------------
 5 files changed, 1194 insertions(+), 456 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/PlaystateController.cs
 create mode 100644 Jellyfin.Api/Helpers/TranscodingJobHelper.cs
 create mode 100644 Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
 create mode 100644 Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/PlaystateService.cs

diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
new file mode 100644
index 0000000000..05a6edf4ed
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Playstate controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class PlaystateController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataRepository;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ISessionManager _sessionManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ILogger<PlaystateController> _logger;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PlaystateController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        public PlaystateController(
+            IUserManager userManager,
+            IUserDataManager userDataRepository,
+            ILibraryManager libraryManager,
+            ISessionManager sessionManager,
+            IAuthorizationContext authContext,
+            ILoggerFactory loggerFactory,
+            IMediaSourceManager mediaSourceManager,
+            IFileSystem fileSystem)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+            _sessionManager = sessionManager;
+            _authContext = authContext;
+            _logger = loggerFactory.CreateLogger<PlaystateController>();
+
+            _transcodingJobHelper = new TranscodingJobHelper(
+                loggerFactory.CreateLogger<TranscodingJobHelper>(),
+                mediaSourceManager,
+                fileSystem);
+        }
+
+        /// <summary>
+        /// Marks an item as played for user.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="datePlayed">Optional. The date the item was played.</param>
+        /// <response code="200">Item marked as played.</response>
+        /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpPost("/Users/{userId}/PlayedItems/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> MarkPlayedItem(
+            [FromRoute] Guid userId,
+            [FromRoute] Guid itemId,
+            [FromQuery] DateTime? datePlayed)
+        {
+            var user = _userManager.GetUserById(userId);
+            var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+            var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+                UpdatePlayedStatus(additionalUser, itemId, true, datePlayed);
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Marks an item as unplayed for user.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <response code="200">Item marked as unplayed.</response>
+        /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
+        [HttpDelete("/Users/{userId}/PlayedItem/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+        {
+            var user = _userManager.GetUserById(userId);
+            var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+            var dto = UpdatePlayedStatus(user, itemId, false, null);
+            foreach (var additionalUserInfo in session.AdditionalUsers)
+            {
+                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+                UpdatePlayedStatus(additionalUser, itemId, false, null);
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Reports playback has started within a session.
+        /// </summary>
+        /// <param name="playbackStartInfo">The playback start info.</param>
+        /// <response code="204">Playback start recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Playing")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
+        {
+            playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
+            playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports playback progress within a session.
+        /// </summary>
+        /// <param name="playbackProgressInfo">The playback progress info.</param>
+        /// <response code="204">Playback progress recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Playing/Progress")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
+        {
+            playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
+            playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Pings a playback session.
+        /// </summary>
+        /// <param name="playSessionId">Playback session id.</param>
+        /// <response code="204">Playback session pinged.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Playing/Ping")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
+        {
+            _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports playback has stopped within a session.
+        /// </summary>
+        /// <param name="playbackStopInfo">The playback stop info.</param>
+        /// <response code="204">Playback stop recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Sessions/Playing/Stopped")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
+        {
+            _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+            if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
+            {
+                await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+            }
+
+            playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that a user has begun playing an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="mediaSourceId">The id of the MediaSource.</param>
+        /// <param name="canSeek">Indicates if the client can seek.</param>
+        /// <param name="audioStreamIndex">The audio stream index.</param>
+        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+        /// <param name="playMethod">The play method.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <response code="204">Play start recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Users/{userId}/PlayingItems/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+        public async Task<ActionResult> OnPlaybackStart(
+            [FromRoute] Guid userId,
+            [FromRoute] Guid itemId,
+            [FromQuery] string mediaSourceId,
+            [FromQuery] bool canSeek,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] PlayMethod playMethod,
+            [FromQuery] string liveStreamId,
+            [FromQuery] string playSessionId)
+        {
+            var playbackStartInfo = new PlaybackStartInfo
+            {
+                CanSeek = canSeek,
+                ItemId = itemId,
+                MediaSourceId = mediaSourceId,
+                AudioStreamIndex = audioStreamIndex,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                PlayMethod = playMethod,
+                PlaySessionId = playSessionId,
+                LiveStreamId = liveStreamId
+            };
+
+            playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
+            playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports a user's playback progress.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="mediaSourceId">The id of the MediaSource.</param>
+        /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="isPaused">Indicates if the player is paused.</param>
+        /// <param name="isMuted">Indicates if the player is muted.</param>
+        /// <param name="audioStreamIndex">The audio stream index.</param>
+        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+        /// <param name="volumeLevel">Scale of 0-100.</param>
+        /// <param name="playMethod">The play method.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="repeatMode">The repeat mode.</param>
+        /// <response code="204">Play progress recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+        public async Task<ActionResult> OnPlaybackProgress(
+            [FromRoute] Guid userId,
+            [FromRoute] Guid itemId,
+            [FromQuery] string mediaSourceId,
+            [FromQuery] long? positionTicks,
+            [FromQuery] bool isPaused,
+            [FromQuery] bool isMuted,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] int? volumeLevel,
+            [FromQuery] PlayMethod playMethod,
+            [FromQuery] string liveStreamId,
+            [FromQuery] string playSessionId,
+            [FromQuery] RepeatMode repeatMode)
+        {
+            var playbackProgressInfo = new PlaybackProgressInfo
+            {
+                ItemId = itemId,
+                PositionTicks = positionTicks,
+                IsMuted = isMuted,
+                IsPaused = isPaused,
+                MediaSourceId = mediaSourceId,
+                AudioStreamIndex = audioStreamIndex,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                VolumeLevel = volumeLevel,
+                PlayMethod = playMethod,
+                PlaySessionId = playSessionId,
+                LiveStreamId = liveStreamId,
+                RepeatMode = repeatMode
+            };
+
+            playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
+            playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Reports that a user has stopped playing an item.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="mediaSourceId">The id of the MediaSource.</param>
+        /// <param name="nextMediaType">The next media type that will play.</param>
+        /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <response code="204">Playback stop recorded.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("/Users/{userId}/PlayingItems/{itemId}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
+        public async Task<ActionResult> OnPlaybackStopped(
+            [FromRoute] Guid userId,
+            [FromRoute] Guid itemId,
+            [FromQuery] string mediaSourceId,
+            [FromQuery] string nextMediaType,
+            [FromQuery] long? positionTicks,
+            [FromQuery] string liveStreamId,
+            [FromQuery] string playSessionId)
+        {
+            var playbackStopInfo = new PlaybackStopInfo
+            {
+                ItemId = itemId,
+                PositionTicks = positionTicks,
+                MediaSourceId = mediaSourceId,
+                PlaySessionId = playSessionId,
+                LiveStreamId = liveStreamId,
+                NextMediaType = nextMediaType
+            };
+
+            _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+            if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
+            {
+                await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+            }
+
+            playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Updates the played status.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+        /// <param name="datePlayed">The date played.</param>
+        /// <returns>Task.</returns>
+        private UserItemDataDto UpdatePlayedStatus(User user, Guid itemId, bool wasPlayed, DateTime? datePlayed)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+
+            if (wasPlayed)
+            {
+                item.MarkPlayed(user, datePlayed, true);
+            }
+            else
+            {
+                item.MarkUnplayed(user);
+            }
+
+            return _userDataRepository.GetUserDataDto(item, user);
+        }
+
+        private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
+        {
+            if (method == PlayMethod.Transcode)
+            {
+                var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
+                if (job == null)
+                {
+                    return PlayMethod.DirectPlay;
+                }
+            }
+
+            return method;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
new file mode 100644
index 0000000000..44f662e6e0
--- /dev/null
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -0,0 +1,354 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Transcoding job helpers.
+    /// </summary>
+    public class TranscodingJobHelper
+    {
+        /// <summary>
+        /// The active transcoding jobs.
+        /// </summary>
+        private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
+
+        /// <summary>
+        /// The transcoding locks.
+        /// </summary>
+        private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
+
+        private readonly ILogger<TranscodingJobHelper> _logger;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        public TranscodingJobHelper(
+            ILogger<TranscodingJobHelper> logger,
+            IMediaSourceManager mediaSourceManager,
+            IFileSystem fileSystem)
+        {
+            _logger = logger;
+            _mediaSourceManager = mediaSourceManager;
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Get transcoding job.
+        /// </summary>
+        /// <param name="playSessionId">Playback session id.</param>
+        /// <returns>The transcoding job.</returns>
+        public TranscodingJobDto GetTranscodingJob(string playSessionId)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
+            }
+        }
+
+        /// <summary>
+        /// Ping transcoding job.
+        /// </summary>
+        /// <param name="playSessionId">Play session id.</param>
+        /// <param name="isUserPaused">Is user paused.</param>
+        /// <exception cref="ArgumentNullException">Play session id is null.</exception>
+        public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+        {
+            if (string.IsNullOrEmpty(playSessionId))
+            {
+                throw new ArgumentNullException(nameof(playSessionId));
+            }
+
+            _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
+
+            List<TranscodingJobDto> jobs;
+
+            lock (_activeTranscodingJobs)
+            {
+                // This is really only needed for HLS.
+                // Progressive streams can stop on their own reliably
+                jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+
+            foreach (var job in jobs)
+            {
+                if (isUserPaused.HasValue)
+                {
+                    _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
+                    job.IsUserPaused = isUserPaused.Value;
+                }
+
+                PingTimer(job, true);
+            }
+        }
+
+        private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
+        {
+            if (job.HasExited)
+            {
+                job.StopKillTimer();
+                return;
+            }
+
+            var timerDuration = 10000;
+
+            if (job.Type != TranscodingJobType.Progressive)
+            {
+                timerDuration = 60000;
+            }
+
+            job.PingTimeout = timerDuration;
+            job.LastPingDate = DateTime.UtcNow;
+
+            // Don't start the timer for playback checkins with progressive streaming
+            if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+            {
+                job.StartKillTimer(OnTranscodeKillTimerStopped);
+            }
+            else
+            {
+                job.ChangeKillTimerIfStarted();
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode kill timer stopped].
+        /// </summary>
+        /// <param name="state">The state.</param>
+        private async void OnTranscodeKillTimerStopped(object state)
+        {
+            var job = (TranscodingJobDto)state;
+
+            if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
+            {
+                var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
+
+                if (timeSinceLastPing < job.PingTimeout)
+                {
+                    job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+                    return;
+                }
+            }
+
+            _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+            await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Kills the single transcoding job.
+        /// </summary>
+        /// <param name="deviceId">The device id.</param>
+        /// <param name="playSessionId">The play session identifier.</param>
+        /// <param name="deleteFiles">The delete files.</param>
+        /// <returns>Task.</returns>
+        public Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
+        {
+            return KillTranscodingJobs(
+                j => string.IsNullOrWhiteSpace(playSessionId)
+                    ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
+                    : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles);
+        }
+
+        /// <summary>
+        /// Kills the transcoding jobs.
+        /// </summary>
+        /// <param name="killJob">The kill job.</param>
+        /// <param name="deleteFiles">The delete files.</param>
+        /// <returns>Task.</returns>
+        private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
+        {
+            var jobs = new List<TranscodingJobDto>();
+
+            lock (_activeTranscodingJobs)
+            {
+                // This is really only needed for HLS.
+                // Progressive streams can stop on their own reliably
+                jobs.AddRange(_activeTranscodingJobs.Where(killJob));
+            }
+
+            if (jobs.Count == 0)
+            {
+                return Task.CompletedTask;
+            }
+
+            IEnumerable<Task> GetKillJobs()
+            {
+                foreach (var job in jobs)
+                {
+                    yield return KillTranscodingJob(job, false, deleteFiles);
+                }
+            }
+
+            return Task.WhenAll(GetKillJobs());
+        }
+
+        /// <summary>
+        /// Kills the transcoding job.
+        /// </summary>
+        /// <param name="job">The job.</param>
+        /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
+        /// <param name="delete">The delete.</param>
+        private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
+        {
+            job.DisposeKillTimer();
+
+            _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+            lock (_activeTranscodingJobs)
+            {
+                _activeTranscodingJobs.Remove(job);
+
+                if (!job.CancellationTokenSource!.IsCancellationRequested)
+                {
+                    job.CancellationTokenSource.Cancel();
+                }
+            }
+
+            lock (_transcodingLocks)
+            {
+                _transcodingLocks.Remove(job.Path!);
+            }
+
+            lock (job.ProcessLock!)
+            {
+                job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+
+                var process = job.Process;
+
+                var hasExited = job.HasExited;
+
+                if (!hasExited)
+                {
+                    try
+                    {
+                        _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
+
+                        process!.StandardInput.WriteLine("q");
+
+                        // Need to wait because killing is asynchronous
+                        if (!process.WaitForExit(5000))
+                        {
+                            _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
+                            process.Kill();
+                        }
+                    }
+                    catch (InvalidOperationException)
+                    {
+                    }
+                }
+            }
+
+            if (delete(job.Path!))
+            {
+                await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+            }
+
+            if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+            {
+                try
+                {
+                    await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+                }
+            }
+        }
+
+        private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+        {
+            if (retryCount >= 10)
+            {
+                return;
+            }
+
+            _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
+
+            await Task.Delay(delayMs).ConfigureAwait(false);
+
+            try
+            {
+                if (jobType == TranscodingJobType.Progressive)
+                {
+                    DeleteProgressivePartialStreamFiles(path);
+                }
+                else
+                {
+                    DeleteHlsPartialStreamFiles(path);
+                }
+            }
+            catch (IOException ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+
+                await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the progressive partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+        {
+            if (File.Exists(outputFilePath))
+            {
+                _fileSystem.DeleteFile(outputFilePath);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the HLS partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        private void DeleteHlsPartialStreamFiles(string outputFilePath)
+        {
+            var directory = Path.GetDirectoryName(outputFilePath);
+            var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+            var filesToDelete = _fileSystem.GetFilePaths(directory)
+                .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+
+            List<Exception>? exs = null;
+            foreach (var file in filesToDelete)
+            {
+                try
+                {
+                    _logger.LogDebug("Deleting HLS file {0}", file);
+                    _fileSystem.DeleteFile(file);
+                }
+                catch (IOException ex)
+                {
+                    (exs ??= new List<Exception>(4)).Add(ex);
+                    _logger.LogError(ex, "Error deleting HLS file {Path}", file);
+                }
+            }
+
+            if (exs != null)
+            {
+                throw new AggregateException("Error deleting HLS files", exs);
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
new file mode 100644
index 0000000000..dcc3224704
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Models.PlaybackDtos
+{
+    /// <summary>
+    /// Class TranscodingJob.
+    /// </summary>
+    public class TranscodingJobDto
+    {
+        /// <summary>
+        /// The process lock.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")]
+        public readonly object ProcessLock = new object();
+
+        /// <summary>
+        /// Timer lock.
+        /// </summary>
+        private readonly object _timerLock = new object();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TranscodingJobDto"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param>
+        public TranscodingJobDto(ILogger<TranscodingJobDto> logger)
+        {
+            Logger = logger;
+        }
+
+        /// <summary>
+        /// Gets or sets the play session identifier.
+        /// </summary>
+        /// <value>The play session identifier.</value>
+        public string? PlaySessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the live stream identifier.
+        /// </summary>
+        /// <value>The live stream identifier.</value>
+        public string? LiveStreamId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether is live output.
+        /// </summary>
+        public bool IsLiveOutput { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public MediaSourceInfo? MediaSource { get; set; }
+
+        /// <summary>
+        /// Gets or sets path.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public TranscodingJobType Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the process.
+        /// </summary>
+        /// <value>The process.</value>
+        public Process? Process { get; set; }
+
+        /// <summary>
+        /// Gets logger.
+        /// </summary>
+        public ILogger<TranscodingJobDto> Logger { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the active request count.
+        /// </summary>
+        /// <value>The active request count.</value>
+        public int ActiveRequestCount { get; set; }
+
+        /// <summary>
+        /// Gets or sets the kill timer.
+        /// </summary>
+        /// <value>The kill timer.</value>
+        private Timer? KillTimer { get; set; }
+
+        /// <summary>
+        /// Gets or sets device id.
+        /// </summary>
+        public string? DeviceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets cancellation token source.
+        /// </summary>
+        public CancellationTokenSource? CancellationTokenSource { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether has exited.
+        /// </summary>
+        public bool HasExited { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether is user paused.
+        /// </summary>
+        public bool IsUserPaused { get; set; }
+
+        /// <summary>
+        /// Gets or sets id.
+        /// </summary>
+        public string? Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets framerate.
+        /// </summary>
+        public float? Framerate { get; set; }
+
+        /// <summary>
+        /// Gets or sets completion percentage.
+        /// </summary>
+        public double? CompletionPercentage { get; set; }
+
+        /// <summary>
+        /// Gets or sets bytes downloaded.
+        /// </summary>
+        public long? BytesDownloaded { get; set; }
+
+        /// <summary>
+        /// Gets or sets bytes transcoded.
+        /// </summary>
+        public long? BytesTranscoded { get; set; }
+
+        /// <summary>
+        /// Gets or sets bit rate.
+        /// </summary>
+        public int? BitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets transcoding position ticks.
+        /// </summary>
+        public long? TranscodingPositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets download position ticks.
+        /// </summary>
+        public long? DownloadPositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets transcoding throttler.
+        /// </summary>
+        public TranscodingThrottler? TranscodingThrottler { get; set; }
+
+        /// <summary>
+        /// Gets or sets last ping date.
+        /// </summary>
+        public DateTime LastPingDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets ping timeout.
+        /// </summary>
+        public int PingTimeout { get; set; }
+
+        /// <summary>
+        /// Stop kill timer.
+        /// </summary>
+        public void StopKillTimer()
+        {
+            lock (_timerLock)
+            {
+                KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+            }
+        }
+
+        /// <summary>
+        /// Dispose kill timer.
+        /// </summary>
+        public void DisposeKillTimer()
+        {
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    KillTimer.Dispose();
+                    KillTimer = null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Start kill timer.
+        /// </summary>
+        /// <param name="callback">Callback action.</param>
+        public void StartKillTimer(Action<object> callback)
+        {
+            StartKillTimer(callback, PingTimeout);
+        }
+
+        /// <summary>
+        /// Start kill timer.
+        /// </summary>
+        /// <param name="callback">Callback action.</param>
+        /// <param name="intervalMs">Callback interval.</param>
+        public void StartKillTimer(Action<object> callback, int intervalMs)
+        {
+            if (HasExited)
+            {
+                return;
+            }
+
+            lock (_timerLock)
+            {
+                if (KillTimer == null)
+                {
+                    Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
+                }
+                else
+                {
+                    Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Change kill timer if started.
+        /// </summary>
+        public void ChangeKillTimerIfStarted()
+        {
+            if (HasExited)
+            {
+                return;
+            }
+
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    var intervalMs = PingTimeout;
+
+                    Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
new file mode 100644
index 0000000000..b5e42ea299
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Models.PlaybackDtos
+{
+    /// <summary>
+    /// Transcoding throttler.
+    /// </summary>
+    public class TranscodingThrottler : IDisposable
+    {
+        private readonly TranscodingJobDto _job;
+        private readonly ILogger<TranscodingThrottler> _logger;
+        private readonly IConfigurationManager _config;
+        private readonly IFileSystem _fileSystem;
+        private Timer? _timer;
+        private bool _isPaused;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TranscodingThrottler"/> class.
+        /// </summary>
+        /// <param name="job">Transcoding job dto.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
+        /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
+        {
+            _job = job;
+            _logger = logger;
+            _config = config;
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Start timer.
+        /// </summary>
+        public void Start()
+        {
+            _timer = new Timer(TimerCallback, null, 5000, 5000);
+        }
+
+        /// <summary>
+        /// Unpause transcoding.
+        /// </summary>
+        /// <returns>A <see cref="Task"/>.</returns>
+        public async Task UnpauseTranscoding()
+        {
+            if (_isPaused)
+            {
+                _logger.LogDebug("Sending resume command to ffmpeg");
+
+                try
+                {
+                    await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
+                    _isPaused = false;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error resuming transcoding");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Stop throttler.
+        /// </summary>
+        /// <returns>A <see cref="Task"/>.</returns>
+        public async Task Stop()
+        {
+            DisposeTimer();
+            await UnpauseTranscoding().ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Dispose throttler.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Dispose throttler.
+        /// </summary>
+        /// <param name="disposing">Disposing.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                DisposeTimer();
+            }
+        }
+
+        private EncodingOptions GetOptions()
+        {
+            return _config.GetConfiguration<EncodingOptions>("encoding");
+        }
+
+        private async void TimerCallback(object state)
+        {
+            if (_job.HasExited)
+            {
+                DisposeTimer();
+                return;
+            }
+
+            var options = GetOptions();
+
+            if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
+            {
+                await PauseTranscoding().ConfigureAwait(false);
+            }
+            else
+            {
+                await UnpauseTranscoding().ConfigureAwait(false);
+            }
+        }
+
+        private async Task PauseTranscoding()
+        {
+            if (!_isPaused)
+            {
+                _logger.LogDebug("Sending pause command to ffmpeg");
+
+                try
+                {
+                    await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
+                    _isPaused = true;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error pausing transcoding");
+                }
+            }
+        }
+
+        private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds)
+        {
+            var bytesDownloaded = job.BytesDownloaded ?? 0;
+            var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
+            var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
+
+            var path = job.Path;
+            var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
+
+            if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
+            {
+                // HLS - time-based consideration
+
+                var targetGap = gapLengthInTicks;
+                var gap = transcodingPositionTicks - downloadPositionTicks;
+
+                if (gap < targetGap)
+                {
+                    _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
+                    return false;
+                }
+
+                _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
+                return true;
+            }
+
+            if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
+            {
+                // Progressive Streaming - byte-based consideration
+
+                try
+                {
+                    var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
+
+                    // Estimate the bytes the transcoder should be ahead
+                    double gapFactor = gapLengthInTicks;
+                    gapFactor /= transcodingPositionTicks;
+                    var targetGap = bytesTranscoded * gapFactor;
+
+                    var gap = bytesTranscoded - bytesDownloaded;
+
+                    if (gap < targetGap)
+                    {
+                        _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
+                        return false;
+                    }
+
+                    _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
+                    return true;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error getting output size");
+                    return false;
+                }
+            }
+
+            _logger.LogDebug("No throttle data for " + path);
+            return false;
+        }
+
+        private void DisposeTimer()
+        {
+            if (_timer != null)
+            {
+                _timer.Dispose();
+                _timer = null;
+            }
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs
deleted file mode 100644
index ab231626bb..0000000000
--- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs
+++ /dev/null
@@ -1,456 +0,0 @@
-using System;
-using System.Globalization;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class MarkPlayedItem
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")]
-    public class MarkPlayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string DatePlayed { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class MarkUnplayedItem
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")]
-    public class MarkUnplayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/Playing", "POST", Summary = "Reports playback has started within a session")]
-    public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Progress", "POST", Summary = "Reports playback progress within a session")]
-    public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Ping", "POST", Summary = "Pings a playback session")]
-    public class PingPlaybackSession : IReturnVoid
-    {
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    [Route("/Sessions/Playing/Stopped", "POST", Summary = "Reports playback has stopped within a session")]
-    public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class OnPlaybackStart
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")]
-    public class OnPlaybackStart : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool CanSeek { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public PlayMethod PlayMethod { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    /// <summary>
-    /// Class OnPlaybackProgress
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")]
-    public class OnPlaybackProgress : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public long? PositionTicks { get; set; }
-
-        [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsPaused { get; set; }
-
-        [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsMuted { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? VolumeLevel { get; set; }
-
-        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public PlayMethod PlayMethod { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-
-        [ApiMember(Name = "RepeatMode", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public RepeatMode RepeatMode { get; set; }
-    }
-
-    /// <summary>
-    /// Class OnPlaybackStopped
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")]
-    public class OnPlaybackStopped : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string NextMediaType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
-        public long? PositionTicks { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    [Authenticated]
-    public class PlaystateService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserDataManager _userDataRepository;
-        private readonly ILibraryManager _libraryManager;
-        private readonly ISessionManager _sessionManager;
-        private readonly ISessionContext _sessionContext;
-        private readonly IAuthorizationContext _authContext;
-
-        public PlaystateService(
-            ILogger<PlaystateService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserDataManager userDataRepository,
-            ILibraryManager libraryManager,
-            ISessionManager sessionManager,
-            ISessionContext sessionContext,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userDataRepository = userDataRepository;
-            _libraryManager = libraryManager;
-            _sessionManager = sessionManager;
-            _sessionContext = sessionContext;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(MarkPlayedItem request)
-        {
-            var result = MarkPlayed(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        private UserItemDataDto MarkPlayed(MarkPlayedItem request)
-        {
-            var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
-            DateTime? datePlayed = null;
-
-            if (!string.IsNullOrEmpty(request.DatePlayed))
-            {
-                datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
-            }
-
-            var session = GetSession(_sessionContext);
-
-            var dto = UpdatePlayedStatus(user, request.Id, true, datePlayed);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
-                UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed);
-            }
-
-            return dto;
-        }
-
-        private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
-        {
-            if (method == PlayMethod.Transcode)
-            {
-                var job = string.IsNullOrWhiteSpace(playSessionId) ? null : ApiEntryPoint.Instance.GetTranscodingJob(playSessionId);
-                if (job == null)
-                {
-                    return PlayMethod.DirectPlay;
-                }
-            }
-
-            return method;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackStart request)
-        {
-            Post(new ReportPlaybackStart
-            {
-                CanSeek = request.CanSeek,
-                ItemId = new Guid(request.Id),
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex,
-                PlayMethod = request.PlayMethod,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId
-            });
-        }
-
-        public void Post(ReportPlaybackStart request)
-        {
-            request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            var task = _sessionManager.OnPlaybackStart(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackProgress request)
-        {
-            Post(new ReportPlaybackProgress
-            {
-                ItemId = new Guid(request.Id),
-                PositionTicks = request.PositionTicks,
-                IsMuted = request.IsMuted,
-                IsPaused = request.IsPaused,
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex,
-                VolumeLevel = request.VolumeLevel,
-                PlayMethod = request.PlayMethod,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId,
-                RepeatMode = request.RepeatMode
-            });
-        }
-
-        public void Post(ReportPlaybackProgress request)
-        {
-            request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            var task = _sessionManager.OnPlaybackProgress(request);
-
-            Task.WaitAll(task);
-        }
-
-        public void Post(PingPlaybackSession request)
-        {
-            ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId, null);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(OnPlaybackStopped request)
-        {
-            return Post(new ReportPlaybackStopped
-            {
-                ItemId = new Guid(request.Id),
-                PositionTicks = request.PositionTicks,
-                MediaSourceId = request.MediaSourceId,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId,
-                NextMediaType = request.NextMediaType
-            });
-        }
-
-        public async Task Post(ReportPlaybackStopped request)
-        {
-            Logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", request.PlaySessionId ?? string.Empty);
-
-            if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
-            {
-                await ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
-            }
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            await _sessionManager.OnPlaybackStopped(request);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(MarkUnplayedItem request)
-        {
-            var task = MarkUnplayed(request);
-
-            return ToOptimizedResult(task);
-        }
-
-        private UserItemDataDto MarkUnplayed(MarkUnplayedItem request)
-        {
-            var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
-            var session = GetSession(_sessionContext);
-
-            var dto = UpdatePlayedStatus(user, request.Id, false, null);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
-                UpdatePlayedStatus(additionalUser, request.Id, false, null);
-            }
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Updates the played status.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
-        /// <param name="datePlayed">The date played.</param>
-        /// <returns>Task.</returns>
-        private UserItemDataDto UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
-        {
-            var item = _libraryManager.GetItemById(itemId);
-
-            if (wasPlayed)
-            {
-                item.MarkPlayed(user, datePlayed, true);
-            }
-            else
-            {
-                item.MarkUnplayed(user);
-            }
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-    }
-}

From bcd3cddad884540d6f8e3caa7545ba8c3d5b637b Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 27 Jun 2020 12:26:43 +0200
Subject: [PATCH 274/463] Move MediaInfoService to Jellyfin.Api

---
 .../Controllers/MediaInfoController.cs        | 773 ++++++++++++++++++
 1 file changed, 773 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/MediaInfoController.cs

diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
new file mode 100644
index 0000000000..daf4bf419b
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Buffers;
+using System.Globalization;
+using System.Linq;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The media info controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class MediaInfoController : BaseJellyfinApiController
+    {
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly INetworkManager _networkManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IUserManager _userManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly ILogger _logger;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
+        /// </summary>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public MediaInfoController(
+            IMediaSourceManager mediaSourceManager,
+            IDeviceManager deviceManager,
+            ILibraryManager libraryManager,
+            INetworkManager networkManager,
+            IMediaEncoder mediaEncoder,
+            IUserManager userManager,
+            IAuthorizationContext authContext,
+            ILogger<MediaInfoController> logger,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _mediaSourceManager = mediaSourceManager;
+            _deviceManager = deviceManager;
+            _libraryManager = libraryManager;
+            _networkManager = networkManager;
+            _mediaEncoder = mediaEncoder;
+            _userManager = userManager;
+            _authContext = authContext;
+            _logger = logger;
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <summary>
+        /// Gets live playback media info for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <response code="200">Playback info returned.</response>
+        /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
+        [HttpGet("/Items/{itemId}/PlaybackInfo")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        {
+            return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets live playback media info for an item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+        /// <param name="startTimeTicks">The start time in ticks.</param>
+        /// <param name="audioStreamIndex">The audio stream index.</param>
+        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+        /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+        /// <param name="mediaSourceId">The media source id.</param>
+        /// <param name="liveStreamId">The livestream id.</param>
+        /// <param name="deviceProfile">The device profile.</param>
+        /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
+        /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+        /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+        /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
+        /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
+        /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
+        /// <response code="200">Playback info returned.</response>
+        /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
+        [HttpPost("/Items/{itemId}/PlaybackInfo")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
+            [FromRoute] Guid itemId,
+            [FromQuery] Guid userId,
+            [FromQuery] long? maxStreamingBitrate,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string mediaSourceId,
+            [FromQuery] string liveStreamId,
+            [FromQuery] DeviceProfile deviceProfile,
+            [FromQuery] bool autoOpenLiveStream,
+            [FromQuery] bool enableDirectPlay = true,
+            [FromQuery] bool enableDirectStream = true,
+            [FromQuery] bool enableTranscoding = true,
+            [FromQuery] bool allowVideoStreamCopy = true,
+            [FromQuery] bool allowAudioStreamCopy = true)
+        {
+            var authInfo = _authContext.GetAuthorizationInfo(Request);
+
+            var profile = deviceProfile;
+
+            _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
+
+            if (profile == null)
+            {
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    profile = caps.DeviceProfile;
+                }
+            }
+
+            var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
+
+            if (profile != null)
+            {
+                // set device specific data
+                var item = _libraryManager.GetItemById(itemId);
+
+                foreach (var mediaSource in info.MediaSources)
+                {
+                    SetDeviceSpecificData(
+                        item,
+                        mediaSource,
+                        profile,
+                        authInfo,
+                        maxStreamingBitrate ?? profile.MaxStreamingBitrate,
+                        startTimeTicks ?? 0,
+                        mediaSourceId,
+                        audioStreamIndex,
+                        subtitleStreamIndex,
+                        maxAudioChannels,
+                        info!.PlaySessionId!,
+                        userId,
+                        enableDirectPlay,
+                        enableDirectStream,
+                        enableTranscoding,
+                        allowVideoStreamCopy,
+                        allowAudioStreamCopy);
+                }
+
+                SortMediaSources(info, maxStreamingBitrate);
+            }
+
+            if (autoOpenLiveStream)
+            {
+                var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
+
+                if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+                {
+                    var openStreamResult = await OpenMediaSource(new LiveStreamRequest
+                    {
+                        AudioStreamIndex = audioStreamIndex,
+                        DeviceProfile = deviceProfile,
+                        EnableDirectPlay = enableDirectPlay,
+                        EnableDirectStream = enableDirectStream,
+                        ItemId = itemId,
+                        MaxAudioChannels = maxAudioChannels,
+                        MaxStreamingBitrate = maxStreamingBitrate,
+                        PlaySessionId = info.PlaySessionId,
+                        StartTimeTicks = startTimeTicks,
+                        SubtitleStreamIndex = subtitleStreamIndex,
+                        UserId = userId,
+                        OpenToken = mediaSource.OpenToken
+                    }).ConfigureAwait(false);
+
+                    info.MediaSources = new[] { openStreamResult.MediaSource };
+                }
+            }
+
+            if (info.MediaSources != null)
+            {
+                foreach (var mediaSource in info.MediaSources)
+                {
+                    NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
+                }
+            }
+
+            return info;
+        }
+
+        /// <summary>
+        /// Opens a media source.
+        /// </summary>
+        /// <param name="openToken">The open token.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+        /// <param name="startTimeTicks">The start time in ticks.</param>
+        /// <param name="audioStreamIndex">The audio stream index.</param>
+        /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+        /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="deviceProfile">The device profile.</param>
+        /// <param name="directPlayProtocols">The direct play protocols. Default: <see cref="MediaProtocol.Http"/>.</param>
+        /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+        /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+        /// <response code="200">Media source opened.</response>
+        /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
+        [HttpPost("/LiveStreams/Open")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
+            [FromQuery] string openToken,
+            [FromQuery] Guid userId,
+            [FromQuery] string playSessionId,
+            [FromQuery] long? maxStreamingBitrate,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] Guid itemId,
+            [FromQuery] DeviceProfile deviceProfile,
+            [FromQuery] MediaProtocol[] directPlayProtocols,
+            [FromQuery] bool enableDirectPlay = true,
+            [FromQuery] bool enableDirectStream = true)
+        {
+            var request = new LiveStreamRequest
+            {
+                OpenToken = openToken,
+                UserId = userId,
+                PlaySessionId = playSessionId,
+                MaxStreamingBitrate = maxStreamingBitrate,
+                StartTimeTicks = startTimeTicks,
+                AudioStreamIndex = audioStreamIndex,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                MaxAudioChannels = maxAudioChannels,
+                ItemId = itemId,
+                DeviceProfile = deviceProfile,
+                EnableDirectPlay = enableDirectPlay,
+                EnableDirectStream = enableDirectStream,
+                DirectPlayProtocols = directPlayProtocols ?? new[] { MediaProtocol.Http }
+            };
+            return await OpenMediaSource(request).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Closes a media source.
+        /// </summary>
+        /// <param name="liveStreamId">The livestream id.</param>
+        /// <response code="204">Livestream closed.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("/LiveStreams/Close")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult CloseLiveStream([FromQuery] string liveStreamId)
+        {
+            _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Tests the network with a request with the size of the bitrate.
+        /// </summary>
+        /// <param name="size">The bitrate. Defaults to 102400.</param>
+        /// <response code="200">Test buffer returned.</response>
+        /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
+        /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
+        [HttpGet("/Playback/BitrateTest")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status400BadRequest)]
+        [Produces(MediaTypeNames.Application.Octet)]
+        public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
+        {
+            const int MaxSize = 10_000_000;
+
+            if (size <= 0)
+            {
+                return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
+            }
+
+            if (size > MaxSize)
+            {
+                return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
+            }
+
+            byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
+            try
+            {
+                new Random().NextBytes(buffer);
+                return File(buffer, MediaTypeNames.Application.Octet);
+            }
+            finally
+            {
+                ArrayPool<byte>.Shared.Return(buffer);
+            }
+        }
+
+        private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
+            Guid id,
+            Guid userId,
+            string? mediaSourceId = null,
+            string? liveStreamId = null)
+        {
+            var user = _userManager.GetUserById(userId);
+            var item = _libraryManager.GetItemById(id);
+            var result = new PlaybackInfoResponse();
+
+            MediaSourceInfo[] mediaSources;
+            if (string.IsNullOrWhiteSpace(liveStreamId))
+            {
+                // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
+                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
+
+                if (string.IsNullOrWhiteSpace(mediaSourceId))
+                {
+                    mediaSources = mediaSourcesList.ToArray();
+                }
+                else
+                {
+                    mediaSources = mediaSourcesList
+                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+                        .ToArray();
+                }
+            }
+            else
+            {
+                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+
+                mediaSources = new[] { mediaSource };
+            }
+
+            if (mediaSources.Length == 0)
+            {
+                result.MediaSources = Array.Empty<MediaSourceInfo>();
+
+                result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
+            }
+            else
+            {
+                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
+                // Should we move this directly into MediaSourceManager?
+                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
+
+                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            }
+
+            return result;
+        }
+
+        private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
+        {
+            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
+        }
+
+        private void SetDeviceSpecificData(
+            BaseItem item,
+            MediaSourceInfo mediaSource,
+            DeviceProfile profile,
+            AuthorizationInfo auth,
+            long? maxBitrate,
+            long startTimeTicks,
+            string mediaSourceId,
+            int? audioStreamIndex,
+            int? subtitleStreamIndex,
+            int? maxAudioChannels,
+            string playSessionId,
+            Guid userId,
+            bool enableDirectPlay,
+            bool enableDirectStream,
+            bool enableTranscoding,
+            bool allowVideoStreamCopy,
+            bool allowAudioStreamCopy)
+        {
+            var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
+
+            var options = new VideoOptions
+            {
+                MediaSources = new[] { mediaSource },
+                Context = EncodingContext.Streaming,
+                DeviceId = auth.DeviceId,
+                ItemId = item.Id,
+                Profile = profile,
+                MaxAudioChannels = maxAudioChannels
+            };
+
+            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
+            {
+                options.MediaSourceId = mediaSourceId;
+                options.AudioStreamIndex = audioStreamIndex;
+                options.SubtitleStreamIndex = subtitleStreamIndex;
+            }
+
+            var user = _userManager.GetUserById(userId);
+
+            if (!enableDirectPlay)
+            {
+                mediaSource.SupportsDirectPlay = false;
+            }
+
+            if (!enableDirectStream)
+            {
+                mediaSource.SupportsDirectStream = false;
+            }
+
+            if (!enableTranscoding)
+            {
+                mediaSource.SupportsTranscoding = false;
+            }
+
+            if (item is Audio)
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+            else
+            {
+                _logger.LogInformation(
+                    "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
+                    user.Username,
+                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
+                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
+                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
+            }
+
+            // Beginning of Playback Determination: Attempt DirectPlay first
+            if (mediaSource.SupportsDirectPlay)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectPlay = false;
+                }
+                else
+                {
+                    var supportsDirectStream = mediaSource.SupportsDirectStream;
+
+                    // Dummy this up to fool StreamBuilder
+                    mediaSource.SupportsDirectStream = true;
+                    options.MaxBitrate = maxBitrate;
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectPlay = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectPlay = false;
+                    }
+
+                    // Set this back to what it was
+                    mediaSource.SupportsDirectStream = supportsDirectStream;
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsDirectStream)
+            {
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    mediaSource.SupportsDirectStream = false;
+                }
+                else
+                {
+                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
+
+                    if (item is Audio)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+                    else if (item is Video)
+                    {
+                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+                        {
+                            options.ForceDirectStream = true;
+                        }
+                    }
+
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                        ? streamBuilder.BuildAudioItem(options)
+                        : streamBuilder.BuildVideoItem(options);
+
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectStream = false;
+                    }
+
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            if (mediaSource.SupportsTranscoding)
+            {
+                options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
+
+                // The MediaSource supports direct stream, now test to see if the client supports it
+                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+                    ? streamBuilder.BuildAudioItem(options)
+                    : streamBuilder.BuildVideoItem(options);
+
+                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+                        streamInfo.StartPositionTicks = startTimeTicks;
+                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+                else
+                {
+                    if (streamInfo != null)
+                    {
+                        streamInfo.PlaySessionId = playSessionId;
+
+                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
+                        {
+                            streamInfo.StartPositionTicks = startTimeTicks;
+                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+
+                            if (!allowVideoStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+                            }
+
+                            if (!allowAudioStreamCopy)
+                            {
+                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                            }
+
+                            mediaSource.TranscodingContainer = streamInfo.Container;
+                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+                        }
+
+                        if (!allowAudioStreamCopy)
+                        {
+                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+                        }
+
+                        mediaSource.TranscodingContainer = streamInfo.Container;
+                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+
+                        // Do this after the above so that StartPositionTicks is set
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
+                }
+            }
+
+            foreach (var attachment in mediaSource.MediaAttachments)
+            {
+                attachment.DeliveryUrl = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "/Videos/{0}/{1}/Attachments/{2}",
+                    item.Id,
+                    mediaSource.Id,
+                    attachment.Index);
+            }
+        }
+
+        private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
+        {
+            var authInfo = _authContext.GetAuthorizationInfo(Request);
+
+            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
+
+            var profile = request.DeviceProfile;
+            if (profile == null)
+            {
+                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
+                if (caps != null)
+                {
+                    profile = caps.DeviceProfile;
+                }
+            }
+
+            if (profile != null)
+            {
+                var item = _libraryManager.GetItemById(request.ItemId);
+
+                SetDeviceSpecificData(
+                    item,
+                    result.MediaSource,
+                    profile,
+                    authInfo,
+                    request.MaxStreamingBitrate,
+                    request.StartTimeTicks ?? 0,
+                    result.MediaSource.Id,
+                    request.AudioStreamIndex,
+                    request.SubtitleStreamIndex,
+                    request.MaxAudioChannels,
+                    request.PlaySessionId,
+                    request.UserId,
+                    request.EnableDirectPlay,
+                    request.EnableDirectStream,
+                    true,
+                    true,
+                    true);
+            }
+            else
+            {
+                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
+                {
+                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
+                }
+            }
+
+            // here was a check if (result.MediaSource != null) but Rider said it will never be null
+            NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
+
+            return result;
+        }
+
+        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
+        {
+            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
+            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
+
+            mediaSource.TranscodeReasons = info.TranscodeReasons;
+
+            foreach (var profile in profiles)
+            {
+                foreach (var stream in mediaSource.MediaStreams)
+                {
+                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
+                    {
+                        stream.DeliveryMethod = profile.DeliveryMethod;
+
+                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
+                        {
+                            stream.DeliveryUrl = profile.Url.TrimStart('-');
+                            stream.IsExternalUrl = profile.IsExternalUrl;
+                        }
+                    }
+                }
+            }
+        }
+
+        private long? GetMaxBitrate(long? clientMaxBitrate, User user)
+        {
+            var maxBitrate = clientMaxBitrate;
+            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+
+            if (remoteClientMaxBitrate <= 0)
+            {
+                remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
+            }
+
+            if (remoteClientMaxBitrate > 0)
+            {
+                var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
+
+                _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
+                if (!isInLocalNetwork)
+                {
+                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
+                }
+            }
+
+            return maxBitrate;
+        }
+
+        private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
+        {
+            var originalList = result.MediaSources.ToList();
+
+            result.MediaSources = result.MediaSources.OrderBy(i =>
+                {
+                    // Nothing beats direct playing a file
+                    if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+                    if (i.SupportsDirectPlay || i.SupportsDirectStream)
+                    {
+                        return 0;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(i =>
+                {
+                    return i.Protocol switch
+                    {
+                        MediaProtocol.File => 0,
+                        _ => 1,
+                    };
+                })
+                .ThenBy(i =>
+                {
+                    if (maxBitrate.HasValue && i.Bitrate.HasValue)
+                    {
+                        return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
+                    }
+
+                    return 1;
+                })
+                .ThenBy(originalList.IndexOf)
+                .ToArray();
+        }
+    }
+}

From 73bcda7eac6d0785745179fe4b7f58b6bc4ec488 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 27 Jun 2020 10:50:44 -0600
Subject: [PATCH 275/463] Make all optional strings nullable

---
 Jellyfin.Api/Controllers/AlbumsController.cs  |  4 +-
 Jellyfin.Api/Controllers/ApiKeyController.cs  |  4 +-
 .../Controllers/CollectionController.cs       |  8 ++--
 .../Controllers/ConfigurationController.cs    |  4 +-
 .../Controllers/DashboardController.cs        |  2 +-
 Jellyfin.Api/Controllers/DevicesController.cs |  8 ++--
 .../DisplayPreferencesController.cs           | 12 ++---
 .../Controllers/ImageByNameController.cs      | 12 ++---
 .../Controllers/InstantMixController.cs       | 16 +++----
 .../Controllers/ItemUpdateController.cs       |  2 +-
 Jellyfin.Api/Controllers/LibraryController.cs | 16 +++----
 .../Controllers/LibraryStructureController.cs | 22 ++++-----
 .../Controllers/NotificationsController.cs    |  4 +-
 Jellyfin.Api/Controllers/PackageController.cs |  8 ++--
 .../Controllers/PlaylistsController.cs        | 14 +++---
 Jellyfin.Api/Controllers/PluginsController.cs |  4 +-
 .../Controllers/RemoteImageController.cs      |  2 +-
 .../Controllers/ScheduledTasksController.cs   |  8 ++--
 Jellyfin.Api/Controllers/SearchController.cs  | 10 ++--
 Jellyfin.Api/Controllers/SessionController.cs | 46 +++++++++----------
 Jellyfin.Api/Controllers/StartupController.cs |  6 +--
 .../Controllers/SubtitleController.cs         | 14 +++---
 Jellyfin.Api/Controllers/SystemController.cs  |  2 +-
 Jellyfin.Api/Controllers/TvShowsController.cs |  8 ++--
 Jellyfin.Api/Controllers/UserController.cs    |  8 ++--
 .../Controllers/UserLibraryController.cs      |  6 +--
 .../Controllers/UserViewsController.cs        |  2 +-
 .../Controllers/VideoAttachmentsController.cs |  2 +-
 Jellyfin.Api/Controllers/VideosController.cs  |  2 +-
 Jellyfin.Api/Controllers/YearsController.cs   | 16 +++----
 Jellyfin.Api/Extensions/DtoExtensions.cs      |  4 +-
 Jellyfin.Api/Helpers/RequestHelpers.cs        | 10 ++--
 Jellyfin.Api/Helpers/SimilarItemsHelper.cs    |  2 +-
 33 files changed, 143 insertions(+), 145 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 622123873d..70315b0a33 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
             [FromRoute] string albumId,
             [FromQuery] Guid userId,
-            [FromQuery] string excludeArtistIds,
+            [FromQuery] string? excludeArtistIds,
             [FromQuery] int? limit)
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
@@ -85,7 +85,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
             [FromRoute] string artistId,
             [FromQuery] Guid userId,
-            [FromQuery] string excludeArtistIds,
+            [FromQuery] string? excludeArtistIds,
             [FromQuery] int? limit)
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index ed521c1fc5..fef4d7262d 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Keys")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CreateKey([FromQuery, Required] string app)
+        public ActionResult CreateKey([FromQuery, Required] string? app)
         {
             _authRepo.Create(new AuthenticationInfo
             {
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Keys/{key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RevokeKey([FromRoute] string key)
+        public ActionResult RevokeKey([FromRoute] string? key)
         {
             _sessionManager.RevokeToken(key);
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 29db0b1782..7ff98b2513 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -51,8 +51,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<CollectionCreationResult> CreateCollection(
-            [FromQuery] string name,
-            [FromQuery] string ids,
+            [FromQuery] string? name,
+            [FromQuery] string? ids,
             [FromQuery] bool isLocked,
             [FromQuery] Guid? parentId)
         {
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds)
+        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
         {
             _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpDelete("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds)
+        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
         {
             _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index d275ed2eba..13933cb33b 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -70,7 +70,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Configuration.</returns>
         [HttpGet("Configuration/{key}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
+        public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
         {
             return _configurationManager.GetConfiguration(key);
         }
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Configuration/{key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
+        public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 6cfee2463f..699ef6bf7b 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/web/ConfigurationPage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult GetDashboardConfigurationPage([FromQuery] string name)
+        public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
         {
             IPlugin? plugin = null;
             Stream? stream = null;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 55ca7b7c0f..3cf7b33785 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string id)
+        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string? id)
         {
             var deviceInfo = _deviceManager.GetDevice(id);
             if (deviceInfo == null)
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string id)
+        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string? id)
         {
             var deviceInfo = _deviceManager.GetDeviceOptions(id);
             if (deviceInfo == null)
@@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateDeviceOptions(
-            [FromQuery, BindRequired] string id,
+            [FromQuery, BindRequired] string? id,
             [FromBody, BindRequired] DeviceOptions deviceOptions)
         {
             var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
@@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
+        public ActionResult DeleteDevice([FromQuery, BindRequired] string? id)
         {
             var existingDevice = _deviceManager.GetDevice(id);
             if (existingDevice == null)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 3f946d9d22..1255e6dab0 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -39,9 +39,9 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{displayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<DisplayPreferences> GetDisplayPreferences(
-            [FromRoute] string displayPreferencesId,
-            [FromQuery] [Required] string userId,
-            [FromQuery] [Required] string client)
+            [FromRoute] string? displayPreferencesId,
+            [FromQuery] [Required] string? userId,
+            [FromQuery] [Required] string? client)
         {
             return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
         }
@@ -59,9 +59,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
         public ActionResult UpdateDisplayPreferences(
-            [FromRoute] string displayPreferencesId,
-            [FromQuery, BindRequired] string userId,
-            [FromQuery, BindRequired] string client,
+            [FromRoute] string? displayPreferencesId,
+            [FromQuery, BindRequired] string? userId,
+            [FromQuery, BindRequired] string? client,
             [FromBody, BindRequired] DisplayPreferences displayPreferences)
         {
             _displayPreferencesRepository.SaveDisplayPreferences(
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 4800c0608f..5244c35b89 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type)
+        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string? name, [FromRoute] string? type)
         {
             var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
                 ? "folder"
@@ -110,8 +110,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetRatingImage(
-            [FromRoute] string theme,
-            [FromRoute] string name)
+            [FromRoute] string? theme,
+            [FromRoute] string? name)
         {
             return GetImageFile(_applicationPaths.RatingsPath, theme, name);
         }
@@ -143,8 +143,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetMediaInfoImage(
-            [FromRoute] string theme,
-            [FromRoute] string name)
+            [FromRoute] string? theme,
+            [FromRoute] string? name)
         {
             return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
         }
@@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="theme">Theme to search.</param>
         /// <param name="name">File name to search for.</param>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name)
+        private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
         {
             var themeFolder = Path.Combine(basePath, theme);
             if (Directory.Exists(themeFolder))
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f1ff770a40..9d945fe2b0 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -135,7 +135,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -167,10 +167,10 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
-            [FromRoute] string name,
+            [FromRoute] string? name,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -239,7 +239,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -274,7 +274,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid id,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 384f250ecc..c9b2aafcca 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Items/{itemId}/ContentType")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType)
+        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 1ecf2ac737..f1106cda60 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -524,7 +524,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Library/Series/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedSeries([FromQuery] string tvdbId)
+        public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
         {
             var series = _libraryManager.GetItemList(new InternalItemsQuery
             {
@@ -554,7 +554,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Library/Movies/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedMovies([FromRoute] string tmdbId, [FromRoute] string imdbId)
+        public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId)
         {
             var movies = _libraryManager.GetItemList(new InternalItemsQuery
             {
@@ -687,10 +687,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
-            [FromQuery] string excludeArtistIds,
+            [FromQuery] string? excludeArtistIds,
             [FromQuery] Guid userId,
             [FromQuery] int? limit,
-            [FromQuery] string fields)
+            [FromQuery] string? fields)
         {
             var item = itemId.Equals(Guid.Empty)
                 ? (!userId.Equals(Guid.Empty)
@@ -737,7 +737,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Libraries/AvailableOptions")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo([FromQuery] string libraryContentType, [FromQuery] bool isNewLibrary)
+        public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo([FromQuery] string? libraryContentType, [FromQuery] bool isNewLibrary)
         {
             var result = new LibraryOptionsResultDto();
 
@@ -877,10 +877,10 @@ namespace Jellyfin.Api.Controllers
 
         private QueryResult<BaseItemDto> GetSimilarItemsResult(
             BaseItem item,
-            string excludeArtistIds,
+            string? excludeArtistIds,
             Guid userId,
             int? limit,
-            string fields,
+            string? fields,
             string[] includeItemTypes,
             bool isMovie)
         {
@@ -942,7 +942,7 @@ namespace Jellyfin.Api.Controllers
             return result;
         }
 
-        private static string[] GetRepresentativeItemTypes(string contentType)
+        private static string[] GetRepresentativeItemTypes(string? contentType)
         {
             return contentType switch
             {
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index e4ac019c9a..0c91f84477 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -72,8 +72,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> AddVirtualFolder(
-            [FromQuery] string name,
-            [FromQuery] string collectionType,
+            [FromQuery] string? name,
+            [FromQuery] string? collectionType,
             [FromQuery] bool refreshLibrary,
             [FromQuery] string[] paths,
             [FromQuery] LibraryOptions libraryOptions)
@@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RemoveVirtualFolder(
-            [FromQuery] string name,
+            [FromQuery] string? name,
             [FromQuery] bool refreshLibrary)
         {
             await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@@ -123,8 +123,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status409Conflict)]
         public ActionResult RenameVirtualFolder(
-            [FromQuery] string name,
-            [FromQuery] string newName,
+            [FromQuery] string? name,
+            [FromQuery] string? newName,
             [FromQuery] bool refreshLibrary)
         {
             if (string.IsNullOrWhiteSpace(name))
@@ -205,8 +205,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Paths")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddMediaPath(
-            [FromQuery] string name,
-            [FromQuery] string path,
+            [FromQuery] string? name,
+            [FromQuery] string? path,
             [FromQuery] MediaPathInfo pathInfo,
             [FromQuery] bool refreshLibrary)
         {
@@ -256,7 +256,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Paths/Update")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateMediaPath(
-            [FromQuery] string name,
+            [FromQuery] string? name,
             [FromQuery] MediaPathInfo pathInfo)
         {
             if (string.IsNullOrWhiteSpace(name))
@@ -280,8 +280,8 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Paths")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveMediaPath(
-            [FromQuery] string name,
-            [FromQuery] string path,
+            [FromQuery] string? name,
+            [FromQuery] string? path,
             [FromQuery] bool refreshLibrary)
         {
             if (string.IsNullOrWhiteSpace(name))
@@ -327,7 +327,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("LibraryOptions")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateLibraryOptions(
-            [FromQuery] string id,
+            [FromQuery] string? id,
             [FromQuery] LibraryOptions libraryOptions)
         {
             var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index cfa7545c96..02aa39b248 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -93,8 +93,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateAdminNotification(
-            [FromQuery] string name,
-            [FromQuery] string description,
+            [FromQuery] string? name,
+            [FromQuery] string? description,
             [FromQuery] string? url,
             [FromQuery] NotificationLevel? level)
         {
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 486575d23a..68ae05658e 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PackageInfo>> GetPackageInfo(
-            [FromRoute] [Required] string name,
+            [FromRoute] [Required] string? name,
             [FromQuery] string? assemblyGuid)
         {
             var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
@@ -80,9 +80,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> InstallPackage(
-            [FromRoute] [Required] string name,
-            [FromQuery] string assemblyGuid,
-            [FromQuery] string version)
+            [FromRoute] [Required] string? name,
+            [FromQuery] string? assemblyGuid,
+            [FromQuery] string? version)
         {
             var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
             var package = _installationManager.GetCompatibleVersions(
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 2dc0d2dc71..d62404fc93 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -84,8 +84,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("{playlistId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddToPlaylist(
-            [FromRoute] string playlistId,
-            [FromQuery] string ids,
+            [FromRoute] string? playlistId,
+            [FromQuery] string? ids,
             [FromQuery] Guid userId)
         {
             _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId);
@@ -103,8 +103,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult MoveItem(
-            [FromRoute] string playlistId,
-            [FromRoute] string itemId,
+            [FromRoute] string? playlistId,
+            [FromRoute] string? itemId,
             [FromRoute] int newIndex)
         {
             _playlistManager.MoveItem(playlistId, itemId, newIndex);
@@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="NoContentResult"/> on success.</returns>
         [HttpDelete("{playlistId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds)
+        public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
         {
             _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
             return NoContent();
@@ -147,11 +147,11 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid userId,
             [FromRoute] int? startIndex,
             [FromRoute] int? limit,
-            [FromRoute] string fields,
+            [FromRoute] string? fields,
             [FromRoute] bool? enableImages,
             [FromRoute] bool? enableUserData,
             [FromRoute] int? imageTypeLimit,
-            [FromRoute] string enableImageTypes)
+            [FromRoute] string? enableImageTypes)
         {
             var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
             if (playlist == null)
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index fd48983ea7..056395a51d 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -166,7 +166,7 @@ namespace Jellyfin.Api.Controllers
         [Obsolete("This endpoint should not be used.")]
         [HttpPost("RegistrationRecords/{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name)
+        public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string? name)
         {
             return new MBRegistrationRecord
             {
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
         [Obsolete("Paid plugins are not supported")]
         [HttpGet("/Registrations/{name}")]
         [ProducesResponseType(StatusCodes.Status501NotImplemented)]
-        public ActionResult GetRegistration([FromRoute] string name)
+        public ActionResult GetRegistration([FromRoute] string? name)
         {
             // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
             // delete all these registration endpoints. They are only kept for compatibility.
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index a0d14be7a5..6fff301297 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> DownloadRemoteImage(
             [FromRoute] Guid itemId,
             [FromQuery, BindRequired] ImageType type,
-            [FromQuery] string imageUrl)
+            [FromQuery] string? imageUrl)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index bf5c3076e0..3df325e3ba 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<TaskInfo> GetTask([FromRoute] string taskId)
+        public ActionResult<TaskInfo> GetTask([FromRoute] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
                 string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult StartTask([FromRoute] string taskId)
+        public ActionResult StartTask([FromRoute] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult StopTask([FromRoute] string taskId)
+        public ActionResult StopTask([FromRoute] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateTask(
-            [FromRoute] string taskId,
+            [FromRoute] string? taskId,
             [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index d971889db8..14dc0815c7 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -81,11 +81,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] Guid userId,
-            [FromQuery, Required] string searchTerm,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string parentId,
+            [FromQuery, Required] string? searchTerm,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? parentId,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSeries,
             [FromQuery] bool? isNews,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 39da4178d6..bd738aa387 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<SessionInfo>> GetSessions(
             [FromQuery] Guid controllableByUserId,
-            [FromQuery] string deviceId,
+            [FromQuery] string? deviceId,
             [FromQuery] int? activeWithinSeconds)
         {
             var result = _sessionManager.Sessions;
@@ -123,10 +123,10 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Viewing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DisplayContent(
-            [FromRoute] string sessionId,
-            [FromQuery] string itemType,
-            [FromQuery] string itemId,
-            [FromQuery] string itemName)
+            [FromRoute] string? sessionId,
+            [FromQuery] string? itemType,
+            [FromQuery] string? itemId,
+            [FromQuery] string? itemName)
         {
             var command = new BrowseRequest
             {
@@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Playing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
-            [FromRoute] string sessionId,
+            [FromRoute] string? sessionId,
             [FromQuery] Guid[] itemIds,
             [FromQuery] long? startPositionTicks,
             [FromQuery] PlayCommand playCommand,
@@ -191,7 +191,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Playing/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
-            [FromRoute] string sessionId,
+            [FromRoute] string? sessionId,
             [FromBody] PlaystateRequest playstateRequest)
         {
             _sessionManager.SendPlaystateCommand(
@@ -213,8 +213,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/System/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
-            [FromRoute] string sessionId,
-            [FromRoute] string command)
+            [FromRoute] string? sessionId,
+            [FromRoute] string? command)
         {
             var name = command;
             if (Enum.TryParse(name, true, out GeneralCommandType commandType))
@@ -244,8 +244,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Command/{Command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
-            [FromRoute] string sessionId,
-            [FromRoute] string command)
+            [FromRoute] string? sessionId,
+            [FromRoute] string? command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
@@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Command")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendFullGeneralCommand(
-            [FromRoute] string sessionId,
+            [FromRoute] string? sessionId,
             [FromBody, Required] GeneralCommand command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -303,9 +303,9 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/Message")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendMessageCommand(
-            [FromRoute] string sessionId,
-            [FromQuery] string text,
-            [FromQuery] string header,
+            [FromRoute] string? sessionId,
+            [FromQuery] string? text,
+            [FromQuery] string? header,
             [FromQuery] long? timeoutMs)
         {
             var command = new MessageCommand
@@ -330,7 +330,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddUserToSession(
-            [FromRoute] string sessionId,
+            [FromRoute] string? sessionId,
             [FromRoute] Guid userId)
         {
             _sessionManager.AddAdditionalUser(sessionId, userId);
@@ -347,7 +347,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("/Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveUserFromSession(
-            [FromRoute] string sessionId,
+            [FromRoute] string? sessionId,
             [FromRoute] Guid userId)
         {
             _sessionManager.RemoveAdditionalUser(sessionId, userId);
@@ -368,9 +368,9 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/Capabilities")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostCapabilities(
-            [FromQuery] string id,
-            [FromQuery] string playableMediaTypes,
-            [FromQuery] string supportedCommands,
+            [FromQuery] string? id,
+            [FromQuery] string? playableMediaTypes,
+            [FromQuery] string? supportedCommands,
             [FromQuery] bool supportsMediaControl,
             [FromQuery] bool supportsSync,
             [FromQuery] bool supportsPersistentIdentifier = true)
@@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/Capabilities/Full")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostFullCapabilities(
-            [FromQuery] string id,
+            [FromQuery] string? id,
             [FromBody, Required] ClientCapabilities capabilities)
         {
             if (string.IsNullOrWhiteSpace(id))
@@ -424,8 +424,8 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/Sessions/Viewing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportViewing(
-            [FromQuery] string sessionId,
-            [FromQuery] string itemId)
+            [FromQuery] string? sessionId,
+            [FromQuery] string? itemId)
         {
             string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
 
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index d96b0f9934..cc1f797b13 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -75,9 +75,9 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Configuration")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateInitialConfiguration(
-            [FromForm] string uiCulture,
-            [FromForm] string metadataCountryCode,
-            [FromForm] string preferredMetadataLanguage)
+            [FromForm] string? uiCulture,
+            [FromForm] string? metadataCountryCode,
+            [FromForm] string? preferredMetadataLanguage)
         {
             _config.Configuration.UICulture = uiCulture;
             _config.Configuration.MetadataCountryCode = metadataCountryCode;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 95cc39524c..baedafaa63 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string language,
+            [FromRoute] string? language,
             [FromQuery] bool? isPerfectMatch)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
@@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string subtitleId)
+            [FromRoute] string? subtitleId)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
 
@@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
-        public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
+        public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string? id)
         {
             var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
 
@@ -186,9 +186,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitle(
             [FromRoute, Required] Guid itemId,
-            [FromRoute, Required] string mediaSourceId,
+            [FromRoute, Required] string? mediaSourceId,
             [FromRoute, Required] int index,
-            [FromRoute, Required] string format,
+            [FromRoute, Required] string? format,
             [FromQuery] long? endPositionTicks,
             [FromQuery] bool copyTimestamps,
             [FromQuery] bool addVttTimeMap,
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> GetSubtitlePlaylist(
             [FromRoute] Guid itemId,
             [FromRoute] int index,
-            [FromRoute] string mediaSourceId,
+            [FromRoute] string? mediaSourceId,
             [FromQuery, Required] int segmentLength)
         {
             var item = (Video)_libraryManager.GetItemById(itemId);
@@ -324,7 +324,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
         private Task<Stream> EncodeSubtitles(
             Guid id,
-            string mediaSourceId,
+            string? mediaSourceId,
             int index,
             string format,
             long startPositionTicks,
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index e33821b248..bc606f7aad 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Logs/Log")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult GetLogFile([FromQuery, Required] string name)
+        public ActionResult GetLogFile([FromQuery, Required] string? name)
         {
             var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
                 .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 6738dd8c85..80b6a24883 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
-            [FromRoute] string seriesId,
+            [FromRoute] string? seriesId,
             [FromQuery] Guid userId,
             [FromQuery] string? fields,
             [FromQuery] int? season,
@@ -311,12 +311,12 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
-            [FromRoute] string seriesId,
+            [FromRoute] string? seriesId,
             [FromQuery] Guid userId,
-            [FromQuery] string fields,
+            [FromQuery] string? fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
-            [FromQuery] string adjacentTo,
+            [FromQuery] string? adjacentTo,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] string? enableImageTypes,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 9f8d564a7d..24194dcc23 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -164,8 +164,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
             [FromRoute, Required] Guid userId,
-            [FromQuery, BindRequired] string pw,
-            [FromQuery, BindRequired] string password)
+            [FromQuery, BindRequired] string? pw,
+            [FromQuery, BindRequired] string? password)
         {
             var user = _userManager.GetUserById(userId);
 
@@ -483,7 +483,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
         [HttpPost("ForgotPassword")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string enteredUsername)
+        public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string? enteredUsername)
         {
             var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
                           || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
@@ -501,7 +501,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
         [HttpPost("ForgotPassword/Pin")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin)
+        public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
         {
             var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
             return result;
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 597e704693..ca804ebc95 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -265,12 +265,12 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
             [FromRoute] Guid userId,
             [FromQuery] Guid parentId,
-            [FromQuery] string fields,
-            [FromQuery] string includeItemTypes,
+            [FromQuery] string? fields,
+            [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
+            [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] int limit = 20,
             [FromQuery] bool groupItems = true)
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 38bf940876..ad8927262b 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid userId,
             [FromQuery] bool? includeExternalContent,
             [FromQuery] bool includeHidden,
-            [FromQuery] string presetViews)
+            [FromQuery] string? presetViews)
         {
             var query = new UserViewQuery
             {
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 943ba8af3d..eef0a93cdf 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -50,7 +50,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<FileStreamResult>> GetAttachment(
             [FromRoute] Guid videoId,
-            [FromRoute] string mediaSourceId,
+            [FromRoute] string? mediaSourceId,
             [FromRoute] int index)
         {
             try
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index effe630a9b..fb1141984d 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
-        public ActionResult MergeVersions([FromQuery] string itemIds)
+        public ActionResult MergeVersions([FromQuery] string? itemIds)
         {
             var items = RequestHelpers.Split(itemIds, ',', true)
                 .Select(i => _libraryManager.GetItemById(i))
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index a036c818c9..a66a3951e1 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -64,16 +64,16 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetYears(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string sortOrder,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string sortBy,
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? sortBy,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
+            [FromQuery] string? enableImageTypes,
             [FromQuery] Guid userId,
             [FromQuery] bool recursive = true,
             [FromQuery] bool? enableImages = true)
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index ac248109d7..e61e9c29d9 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Extensions
         /// <param name="dtoOptions">DtoOptions object.</param>
         /// <param name="fields">Comma delimited string of fields.</param>
         /// <returns>Modified DtoOptions object.</returns>
-        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields)
+        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields)
         {
             if (string.IsNullOrEmpty(fields))
             {
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Extensions
             bool? enableImages,
             bool? enableUserData,
             int? imageTypeLimit,
-            string enableImageTypes)
+            string? enableImageTypes)
         {
             dtoOptions.EnableImages = enableImages ?? true;
 
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index a8ba98f1f6..fd86feb8b1 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="separator">The char that separates the substrings.</param>
         /// <param name="removeEmpty">Option to remove empty substrings from the array.</param>
         /// <returns>An array of the substrings.</returns>
-        internal static string[] Split(string value, char separator, bool removeEmpty)
+        internal static string[] Split(string? value, char separator, bool removeEmpty)
         {
             if (string.IsNullOrWhiteSpace(value))
             {
@@ -99,16 +99,14 @@ namespace Jellyfin.Api.Helpers
         /// <param name="sortBy">Sort by.</param>
         /// <param name="requestedSortOrder">Sort order.</param>
         /// <returns>Resulting order by.</returns>
-        internal static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
+        internal static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
         {
-            var val = sortBy;
-
-            if (string.IsNullOrEmpty(val))
+            if (string.IsNullOrEmpty(sortBy))
             {
                 return Array.Empty<ValueTuple<string, SortOrder>>();
             }
 
-            var vals = val.Split(',');
+            var vals = sortBy.Split(',');
             if (string.IsNullOrWhiteSpace(requestedSortOrder))
             {
                 requestedSortOrder = "Ascending";
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
index 751e3c4815..fd0c315048 100644
--- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Helpers
             IDtoService dtoService,
             Guid userId,
             string id,
-            string excludeArtistIds,
+            string? excludeArtistIds,
             int? limit,
             Type[] includeTypes,
             Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)

From 68dd31d00e40d85a6dd63333e6249e9541098ea9 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 28 Jun 2020 12:44:17 +0200
Subject: [PATCH 276/463] Remove routes in old service

---
 MediaBrowser.Api/Playback/MediaInfoService.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index 2c6534cc08..89e8c71921 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -28,7 +28,6 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api.Playback
 {
-    [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
     public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
     {
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
@@ -38,24 +37,20 @@ namespace MediaBrowser.Api.Playback
         public Guid UserId { get; set; }
     }
 
-    [Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
     public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
     {
     }
 
-    [Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
     public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
     {
     }
 
-    [Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
     public class CloseMediaSource : IReturnVoid
     {
         [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         public string LiveStreamId { get; set; }
     }
 
-    [Route("/Playback/BitrateTest", "GET")]
     public class GetBitrateTestBytes
     {
         [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]

From 8459d486de37a125433adcc6663d42a87091740e Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Mon, 29 Jun 2020 07:35:00 -0600
Subject: [PATCH 277/463] Update Jellyfin.Api/Controllers/MoviesController.cs

Co-authored-by: David <davidullmer@outlook.de>
---
 Jellyfin.Api/Controllers/MoviesController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index ef2de07e86..4dd3613c6b 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string fields,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? fields,
             [FromQuery] int categoryLimit = 5,
             [FromQuery] int itemLimit = 8)
         {

From 45e034e9a1e02fc1ac2632222a7f232a2008ce08 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 29 Jun 2020 16:34:00 +0200
Subject: [PATCH 278/463] Move ArtistsService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/ArtistsController.cs | 488 ++++++++++++++++++
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  14 +
 .../UserLibrary/ArtistsService.cs             | 143 -----
 3 files changed, 502 insertions(+), 143 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ArtistsController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/ArtistsService.cs

diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
new file mode 100644
index 0000000000..ae81818b81
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -0,0 +1,488 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The artists controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    [Route("/Artists")]
+    public class ArtistsController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public ArtistsController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets all artists from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">Optional. Search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Total record count.</param>
+        /// <response code="200">Artists returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the artists.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetArtists(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+            var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+            var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = excludeItemTypesArr,
+                IncludeItemTypes = includeItemTypesArr,
+                MediaTypes = mediaTypesArr,
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|').Select(i =>
+                {
+                    try
+                    {
+                        return _libraryManager.GetStudio(i);
+                    }
+                    catch
+                    {
+                        return null;
+                    }
+                }).Where(i => i != null).Select(i => i!.Id).ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = _libraryManager.GetArtists(query);
+
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, itemCounts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = itemCounts.ItemCount;
+                    dto.ProgramCount = itemCounts.ProgramCount;
+                    dto.SeriesCount = itemCounts.SeriesCount;
+                    dto.EpisodeCount = itemCounts.EpisodeCount;
+                    dto.MovieCount = itemCounts.MovieCount;
+                    dto.TrailerCount = itemCounts.TrailerCount;
+                    dto.AlbumCount = itemCounts.AlbumCount;
+                    dto.SongCount = itemCounts.SongCount;
+                    dto.ArtistCount = itemCounts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets all album artists from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">Optional. Search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Total record count.</param>
+        /// <response code="200">Album artists returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
+        [HttpGet("/AlbumArtists")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
+            var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
+            var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = excludeItemTypesArr,
+                IncludeItemTypes = includeItemTypesArr,
+                MediaTypes = mediaTypesArr,
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|').Select(i =>
+                {
+                    try
+                    {
+                        return _libraryManager.GetStudio(i);
+                    }
+                    catch
+                    {
+                        return null;
+                    }
+                }).Where(i => i != null).Select(i => i!.Id).ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = _libraryManager.GetAlbumArtists(query);
+
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, itemCounts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = itemCounts.ItemCount;
+                    dto.ProgramCount = itemCounts.ProgramCount;
+                    dto.SeriesCount = itemCounts.SeriesCount;
+                    dto.EpisodeCount = itemCounts.EpisodeCount;
+                    dto.MovieCount = itemCounts.MovieCount;
+                    dto.TrailerCount = itemCounts.TrailerCount;
+                    dto.AlbumCount = itemCounts.AlbumCount;
+                    dto.SongCount = itemCounts.SongCount;
+                    dto.ArtistCount = itemCounts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets an artist by name.
+        /// </summary>
+        /// <param name="name">Studio name.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Artist returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the artist.</returns>
+        [HttpGet("{name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid userId)
+        {
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            var item = _libraryManager.GetArtist(name, dtoOptions);
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index fd86feb8b1..dec8fd95eb 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -4,6 +4,7 @@ using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers
@@ -130,5 +131,18 @@ namespace Jellyfin.Api.Helpers
 
             return result;
         }
+
+        /// <summary>
+        /// Gets the filters.
+        /// </summary>
+        /// <param name="filters">The filter string.</param>
+        /// <returns>IEnumerable{ItemFilter}.</returns>
+        internal static ItemFilter[] GetFilters(string filters)
+        {
+            return string.IsNullOrEmpty(filters)
+                ? Array.Empty<ItemFilter>()
+                : Split(filters, ',', true)
+                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
+        }
     }
 }
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
deleted file mode 100644
index bef91d54df..0000000000
--- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetArtists
-    /// </summary>
-    [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")]
-    public class GetArtists : GetItemsByName
-    {
-    }
-
-    [Route("/Artists/AlbumArtists", "GET", Summary = "Gets all album artists from a given item, folder, or the entire library")]
-    public class GetAlbumArtists : GetItemsByName
-    {
-    }
-
-    [Route("/Artists/{Name}", "GET", Summary = "Gets an artist, by name")]
-    public class GetArtist : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class ArtistsService
-    /// </summary>
-    [Authenticated]
-    public class ArtistsService : BaseItemsByNameService<MusicArtist>
-    {
-        public ArtistsService(
-            ILogger<ArtistsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetArtist request)
-        {
-            return GetItem(request);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetArtist request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetArtist(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetArtists request)
-        {
-            return GetResultSlim(request);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetAlbumArtists request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

From 603b1693c0125664a37a6bef13f459b540322b6c Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 30 Jun 2020 14:15:17 +0200
Subject: [PATCH 279/463] Update Jellyfin.Api/Controllers/ArtistsController.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/ArtistsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index ae81818b81..6b2084170e 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -286,7 +286,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableTotalRecordCount">Total record count.</param>
         /// <response code="200">Album artists returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
-        [HttpGet("/AlbumArtists")]
+        [HttpGet("AlbumArtists")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
             [FromQuery] double? minCommunityRating,

From 6385e1ba4633ab5dba171a9d3d4a5374dd70d3d4 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 30 Jun 2020 14:36:45 +0200
Subject: [PATCH 280/463] Fix Build

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index c3cebdef60..f10c8597ec 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers

From 5dfe1ed9b31fe4e37b546317f0d644ccc28df64e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 30 Jun 2020 15:01:08 +0200
Subject: [PATCH 281/463] Fix using ordering

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index f10c8597ec..dec8fd95eb 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -3,8 +3,8 @@ using System.Linq;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
 namespace Jellyfin.Api.Helpers

From 13c4cb628f5f97e425bd4d392dff0d81130131db Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 30 Jun 2020 18:03:04 -0600
Subject: [PATCH 282/463] add missing function after merge

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index dec8fd95eb..a632cbead0 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -144,5 +144,31 @@ namespace Jellyfin.Api.Helpers
                 : Split(filters, ',', true)
                     .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
         }
+
+        /// <summary>
+        /// Gets the item fields.
+        /// </summary>
+        /// <param name="fields">The fields string.</param>
+        /// <returns>IEnumerable{ItemFields}.</returns>
+        internal static ItemFields[] GetItemFields(string? fields)
+        {
+            if (string.IsNullOrEmpty(fields))
+            {
+                return Array.Empty<ItemFields>();
+            }
+
+            return Split(fields, ',', true)
+                .Select(v =>
+                {
+                    if (Enum.TryParse(v, true, out ItemFields value))
+                    {
+                        return (ItemFields?)value;
+                    }
+
+                    return null;
+                }).Where(i => i.HasValue)
+                .Select(i => i!.Value)
+                .ToArray();
+        }
     }
 }

From 1aaee5cf8f78fe00defd78ad7521496772c7b27b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 30 Jun 2020 18:26:53 -0600
Subject: [PATCH 283/463] Move PersonsService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/PersonsController.cs | 282 ++++++++++++++++++
 .../UserLibrary/PersonsService.cs             | 146 ---------
 2 files changed, 282 insertions(+), 146 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/PersonsController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/PersonsService.cs

diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
new file mode 100644
index 0000000000..03478a20a4
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Persons controller.
+    /// </summary>
+    public class PersonsController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PersonsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        public PersonsController(
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Gets all persons from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">The search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
+        /// <response code="200">Persons returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetPersons(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|')
+                    .Select(i =>
+                    {
+                        try
+                        {
+                            return _libraryManager.GetStudio(i);
+                        }
+                        catch
+                        {
+                            return null;
+                        }
+                    }).Where(i => i != null)
+                    .Select(i => i!.Id)
+                    .ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = new QueryResult<(BaseItem, ItemCounts)>();
+
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, counts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = counts.ItemCount;
+                    dto.ProgramCount = counts.ProgramCount;
+                    dto.SeriesCount = counts.SeriesCount;
+                    dto.EpisodeCount = counts.EpisodeCount;
+                    dto.MovieCount = counts.MovieCount;
+                    dto.TrailerCount = counts.TrailerCount;
+                    dto.AlbumCount = counts.AlbumCount;
+                    dto.SongCount = counts.SongCount;
+                    dto.ArtistCount = counts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Get person by name.
+        /// </summary>
+        /// <param name="name">Person name.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <response code="200">Person returned.</response>
+        /// <response code="404">Person not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the person on success,
+        /// or a <see cref="NotFoundResult"/> if person not found.</returns>
+        [HttpGet("{name}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid userId)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddClientFields(Request);
+
+            var item = _libraryManager.GetPerson(name);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs
deleted file mode 100644
index 3204e5219f..0000000000
--- a/MediaBrowser.Api/UserLibrary/PersonsService.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetPersons
-    /// </summary>
-    [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")]
-    public class GetPersons : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetPerson
-    /// </summary>
-    [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")]
-    public class GetPerson : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class PersonsService
-    /// </summary>
-    [Authenticated]
-    public class PersonsService : BaseItemsByNameService<Person>
-    {
-        public PersonsService(
-            ILogger<PersonsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPerson request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetPerson request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetPerson(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPersons request)
-        {
-            return GetResultSlim(request);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            var items = LibraryManager.GetPeopleItems(new InternalPeopleQuery
-            {
-                PersonTypes = query.PersonTypes,
-                NameContains = query.NameContains ?? query.SearchTerm
-            });
-
-            if ((query.IsFavorite ?? false) && query.User != null)
-            {
-                items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList();
-            }
-
-            return new QueryResult<(BaseItem, ItemCounts)>
-            {
-                TotalRecordCount = items.Count,
-                Items = items.Take(query.Limit ?? int.MaxValue).Select(i => (i as BaseItem, new ItemCounts())).ToArray()
-            };
-        }
-    }
-}

From 0830d381c49b59d4a2ca327a79cfd4b7d8b7df0c Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Fri, 3 Jul 2020 10:25:26 -0600
Subject: [PATCH 284/463] Add missing endpoint

---
 Jellyfin.Api/Controllers/LiveTvController.cs | 23 +++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index aca295419e..580bf849a6 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -724,6 +724,27 @@ namespace Jellyfin.Api.Controllers
             return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
         }
 
+        /// <summary>
+        /// Gets a live tv program.
+        /// </summary>
+        /// <param name="programId">Program id.</param>
+        /// <param name="userId">Optional. Attach user data.</param>
+        /// <response code="200">Program returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
+        [HttpGet("Programs/{programId{")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult<BaseItemDto>> GetProgram(
+            [FromRoute] string programId,
+            [FromQuery] Guid userId)
+        {
+            var user = userId.Equals(Guid.Empty)
+                ? null
+                : _userManager.GetUserById(userId);
+
+            return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Deletes a live tv recording.
         /// </summary>
@@ -779,7 +800,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Timers/{timerId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
         {
             AssertUserCanManageLiveTv();

From 121de44ad099229c6fd7db03dc44f94e16569f85 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 3 Jul 2020 18:39:50 +0200
Subject: [PATCH 285/463] Move ItemsService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/ItemsController.cs  | 585 +++++++++++++++++++
 MediaBrowser.Api/UserLibrary/ItemsService.cs |   3 -
 2 files changed, 585 insertions(+), 3 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/ItemsController.cs

diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
new file mode 100644
index 0000000000..161d30e876
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -0,0 +1,585 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The items controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class ItemsController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILocalizationManager _localization;
+        private readonly IDtoService _dtoService;
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemsController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+        public ItemsController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            ILocalizationManager localization,
+            IDtoService dtoService,
+            ILogger<ItemsController> logger)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _localization = localization;
+            _dtoService = dtoService;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Gets items based on a query.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+        /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+        /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+        /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+        /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+        /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+        /// <param name="isHd">Optional filter by items that are HD or not.</param>
+        /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+        /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+        /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+        /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+        /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+        /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+        /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+        /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+        /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
+        /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+        /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+        /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+        /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+        /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+        /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
+        /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+        /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+        /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+        /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="isLocked">Optional filter by items that are locked.</param>
+        /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+        /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+        /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+        /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+        /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+        /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+        /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+        /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+        /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+        /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
+        [HttpGet("/Items")]
+        [HttpGet("/Users/{userId}/Items")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetItems(
+            [FromRoute] Guid userId,
+            [FromQuery] string? maxOfficialRating,
+            [FromQuery] bool? hasThemeSong,
+            [FromQuery] bool? hasThemeVideo,
+            [FromQuery] bool? hasSubtitles,
+            [FromQuery] bool? hasSpecialFeature,
+            [FromQuery] bool? hasTrailer,
+            [FromQuery] string? adjacentTo,
+            [FromQuery] int? parentIndexNumber,
+            [FromQuery] bool? hasParentalRating,
+            [FromQuery] bool? isHd,
+            [FromQuery] bool? is4K,
+            [FromQuery] string? locationTypes,
+            [FromQuery] string? excludeLocationTypes,
+            [FromQuery] bool? isMissing,
+            [FromQuery] bool? isUnaired,
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] double? minCriticRating,
+            [FromQuery] DateTime? minPremiereDate,
+            [FromQuery] DateTime? minDateLastSaved,
+            [FromQuery] DateTime? minDateLastSavedForUser,
+            [FromQuery] DateTime? maxPremiereDate,
+            [FromQuery] bool? hasOverview,
+            [FromQuery] bool? hasImdbId,
+            [FromQuery] bool? hasTmdbId,
+            [FromQuery] bool? hasTvdbId,
+            [FromQuery] string? excludeItemIds,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] bool? recursive,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? imageTypes,
+            [FromQuery] string? sortBy,
+            [FromQuery] bool? isPlayed,
+            [FromQuery] string? genres,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? artists,
+            [FromQuery] string? excludeArtistIds,
+            [FromQuery] string? artistIds,
+            [FromQuery] string? albumArtistIds,
+            [FromQuery] string? contributingArtistIds,
+            [FromQuery] string? albums,
+            [FromQuery] string? albumIds,
+            [FromQuery] string? ids,
+            [FromQuery] string? videoTypes,
+            [FromQuery] string? minOfficialRating,
+            [FromQuery] bool? isLocked,
+            [FromQuery] bool? isPlaceHolder,
+            [FromQuery] bool? hasOfficialRating,
+            [FromQuery] bool? collapseBoxSetItems,
+            [FromQuery] int? minWidth,
+            [FromQuery] int? minHeight,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] bool? is3D,
+            [FromQuery] string? seriesStatus,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
+            [FromQuery] string? studioIds,
+            [FromQuery] string? genreIds,
+            [FromQuery] bool enableTotalRecordCount = true,
+            [FromQuery] bool? enableImages = true)
+        {
+            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+            {
+                parentId = null;
+            }
+
+            BaseItem? item = null;
+            QueryResult<BaseItem> result;
+            if (!string.IsNullOrEmpty(parentId))
+            {
+                item = _libraryManager.GetItemById(parentId);
+            }
+
+            item ??= _libraryManager.GetUserRootFolder();
+
+            if (!(item is Folder folder))
+            {
+                folder = _libraryManager.GetUserRootFolder();
+            }
+
+            if (folder is IHasCollectionType hasCollectionType
+                && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+            {
+                recursive = true;
+                includeItemTypes = "Playlist";
+            }
+
+            bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
+                                     // Assume all folders inside an EnabledChannel are enabled
+                                     || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
+
+            var collectionFolders = _libraryManager.GetCollectionFolders(item);
+            foreach (var collectionFolder in collectionFolders)
+            {
+                if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
+                    collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+                    StringComparer.OrdinalIgnoreCase))
+                {
+                    isInEnabledFolder = true;
+                }
+            }
+
+            if (!(item is UserRootFolder)
+                && !isInEnabledFolder
+                && !user.HasPermission(PermissionKind.EnableAllFolders)
+                && !user.HasPermission(PermissionKind.EnableAllChannels))
+            {
+                _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
+                return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
+            }
+
+            if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
+            {
+                var query = new InternalItemsQuery(user!)
+                {
+                    IsPlayed = isPlayed,
+                    MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+                    IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                    ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                    Recursive = recursive!.Value,
+                    OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+                    IsFavorite = isFavorite,
+                    Limit = limit,
+                    StartIndex = startIndex,
+                    IsMissing = isMissing,
+                    IsUnaired = isUnaired,
+                    CollapseBoxSetItems = collapseBoxSetItems,
+                    NameLessThan = nameLessThan,
+                    NameStartsWith = nameStartsWith,
+                    NameStartsWithOrGreater = nameStartsWithOrGreater,
+                    HasImdbId = hasImdbId,
+                    IsPlaceHolder = isPlaceHolder,
+                    IsLocked = isLocked,
+                    MinWidth = minWidth,
+                    MinHeight = minHeight,
+                    MaxWidth = maxWidth,
+                    MaxHeight = maxHeight,
+                    Is3D = is3D,
+                    HasTvdbId = hasTvdbId,
+                    HasTmdbId = hasTmdbId,
+                    HasOverview = hasOverview,
+                    HasOfficialRating = hasOfficialRating,
+                    HasParentalRating = hasParentalRating,
+                    HasSpecialFeature = hasSpecialFeature,
+                    HasSubtitles = hasSubtitles,
+                    HasThemeSong = hasThemeSong,
+                    HasThemeVideo = hasThemeVideo,
+                    HasTrailer = hasTrailer,
+                    IsHD = isHd,
+                    Is4K = is4K,
+                    Tags = RequestHelpers.Split(tags, '|', true),
+                    OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+                    Genres = RequestHelpers.Split(genres, '|', true),
+                    ArtistIds = RequestHelpers.GetGuids(artistIds),
+                    AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
+                    ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
+                    GenreIds = RequestHelpers.GetGuids(genreIds),
+                    StudioIds = RequestHelpers.GetGuids(studioIds),
+                    Person = person,
+                    PersonIds = RequestHelpers.GetGuids(personIds),
+                    PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                    Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+                    ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
+                    VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
+                    AdjacentTo = adjacentTo,
+                    ItemIds = RequestHelpers.GetGuids(ids),
+                    MinCommunityRating = minCommunityRating,
+                    MinCriticRating = minCriticRating,
+                    ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
+                    ParentIndexNumber = parentIndexNumber,
+                    EnableTotalRecordCount = enableTotalRecordCount,
+                    ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
+                    DtoOptions = dtoOptions,
+                    SearchTerm = searchTerm,
+                    MinDateLastSaved = minDateLastSaved!.Value.ToUniversalTime(),
+                    MinDateLastSavedForUser = minDateLastSavedForUser!.Value.ToUniversalTime(),
+                    MinPremiereDate = minPremiereDate!.Value.ToUniversalTime(),
+                    MaxPremiereDate = maxPremiereDate!.Value.ToUniversalTime(),
+                };
+
+                if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
+                {
+                    query.CollapseBoxSetItems = false;
+                }
+
+                foreach (var filter in RequestHelpers.GetFilters(filters!))
+                {
+                    switch (filter)
+                    {
+                        case ItemFilter.Dislikes:
+                            query.IsLiked = false;
+                            break;
+                        case ItemFilter.IsFavorite:
+                            query.IsFavorite = true;
+                            break;
+                        case ItemFilter.IsFavoriteOrLikes:
+                            query.IsFavoriteOrLiked = true;
+                            break;
+                        case ItemFilter.IsFolder:
+                            query.IsFolder = true;
+                            break;
+                        case ItemFilter.IsNotFolder:
+                            query.IsFolder = false;
+                            break;
+                        case ItemFilter.IsPlayed:
+                            query.IsPlayed = true;
+                            break;
+                        case ItemFilter.IsResumable:
+                            query.IsResumable = true;
+                            break;
+                        case ItemFilter.IsUnplayed:
+                            query.IsPlayed = false;
+                            break;
+                        case ItemFilter.Likes:
+                            query.IsLiked = true;
+                            break;
+                    }
+                }
+
+                // Filter by Series Status
+                if (!string.IsNullOrEmpty(seriesStatus))
+                {
+                    query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
+                }
+
+                // ExcludeLocationTypes
+                if (!string.IsNullOrEmpty(excludeLocationTypes))
+                {
+                    if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
+                    {
+                        query.IsVirtualItem = false;
+                    }
+                }
+
+                if (!string.IsNullOrEmpty(locationTypes))
+                {
+                    var requestedLocationTypes = locationTypes.Split(',');
+                    if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
+                    {
+                        query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
+                    }
+                }
+
+                // Min official rating
+                if (!string.IsNullOrWhiteSpace(minOfficialRating))
+                {
+                    query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
+                }
+
+                // Max official rating
+                if (!string.IsNullOrWhiteSpace(maxOfficialRating))
+                {
+                    query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
+                }
+
+                // Artists
+                if (!string.IsNullOrEmpty(artists))
+                {
+                    query.ArtistIds = artists.Split('|').Select(i =>
+                    {
+                        try
+                        {
+                            return _libraryManager.GetArtist(i, new DtoOptions(false));
+                        }
+                        catch
+                        {
+                            return null;
+                        }
+                    }).Where(i => i != null).Select(i => i!.Id).ToArray();
+                }
+
+                // ExcludeArtistIds
+                if (!string.IsNullOrWhiteSpace(excludeArtistIds))
+                {
+                    query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+                }
+
+                if (!string.IsNullOrWhiteSpace(albumIds))
+                {
+                    query.AlbumIds = RequestHelpers.GetGuids(albumIds);
+                }
+
+                // Albums
+                if (!string.IsNullOrEmpty(albums))
+                {
+                    query.AlbumIds = albums.Split('|').SelectMany(i =>
+                    {
+                        return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
+                    }).ToArray();
+                }
+
+                // Studios
+                if (!string.IsNullOrEmpty(studios))
+                {
+                    query.StudioIds = studios.Split('|').Select(i =>
+                    {
+                        try
+                        {
+                            return _libraryManager.GetStudio(i);
+                        }
+                        catch
+                        {
+                            return null;
+                        }
+                    }).Where(i => i != null).Select(i => i!.Id).ToArray();
+                }
+
+                // Apply default sorting if none requested
+                if (query.OrderBy.Count == 0)
+                {
+                    // Albums by artist
+                    if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
+                    {
+                        query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
+                    }
+                }
+
+                result = folder.GetItems(query);
+            }
+            else
+            {
+                var itemsArray = folder.GetChildren(user, true);
+                result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 };
+            }
+
+            return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
+        }
+
+        /// <summary>
+        /// Gets items based on a query.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="startIndex">The start index.</param>
+        /// <param name="limit">The item limit.</param>
+        /// <param name="searchTerm">The search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+        /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <response code="200">Items returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
+        [HttpGet("/Users/{userId}/Items/Resume")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
+            [FromRoute] Guid userId,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] bool enableTotalRecordCount = true,
+            [FromQuery] bool? enableImages = true)
+        {
+            var user = _userManager.GetUserById(userId);
+            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            var ancestorIds = Array.Empty<Guid>();
+
+            var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
+            if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
+            {
+                ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+                    .Where(i => i is Folder)
+                    .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+                    .Select(i => i.Id)
+                    .ToArray();
+            }
+
+            var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+            {
+                OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
+                IsResumable = true,
+                StartIndex = startIndex,
+                Limit = limit,
+                ParentId = parentIdGuid,
+                Recursive = true,
+                DtoOptions = dtoOptions,
+                MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+                IsVirtualItem = false,
+                CollapseBoxSetItems = false,
+                EnableTotalRecordCount = enableTotalRecordCount,
+                AncestorIds = ancestorIds,
+                IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                SearchTerm = searchTerm
+            });
+
+            var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                StartIndex = startIndex.GetValueOrDefault(),
+                TotalRecordCount = itemsResult.TotalRecordCount,
+                Items = returnItems
+            };
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index 49d534c364..04c3b97c07 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -22,13 +22,10 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetItems
     /// </summary>
-    [Route("/Items", "GET", Summary = "Gets items based on a query.")]
-    [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")]
     public class GetItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
     {
     }
 
-    [Route("/Users/{UserId}/Items/Resume", "GET", Summary = "Gets items based on a query.")]
     public class GetResumeItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
     {
     }

From 31a492b2b9bfbf971749a628b4e88ad3c4923b20 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 3 Jul 2020 19:02:07 +0200
Subject: [PATCH 286/463] Move TrailersService to Jellyfin.Api

---
 .../Controllers/TrailersController.cs         | 306 +++++++++++
 MediaBrowser.Api/Movies/TrailersService.cs    |  89 ---
 MediaBrowser.Api/UserLibrary/ItemsService.cs  | 511 ------------------
 3 files changed, 306 insertions(+), 600 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/TrailersController.cs
 delete mode 100644 MediaBrowser.Api/Movies/TrailersService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/ItemsService.cs

diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
new file mode 100644
index 0000000000..39931f1976
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -0,0 +1,306 @@
+using System;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The trailers controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class TrailersController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILogger<ItemsController> _logger;
+        private readonly IDtoService _dtoService;
+        private readonly ILocalizationManager _localizationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TrailersController"/> class.
+        /// </summary>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        public TrailersController(
+            ILoggerFactory loggerFactory,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService,
+            ILocalizationManager localizationManager)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+            _localizationManager = localizationManager;
+            _logger = loggerFactory.CreateLogger<ItemsController>();
+        }
+
+        /// <summary>
+        /// Finds movies and trailers similar to a given trailer.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+        /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+        /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+        /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+        /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+        /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+        /// <param name="isHd">Optional filter by items that are HD or not.</param>
+        /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+        /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+        /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+        /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+        /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+        /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+        /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+        /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+        /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
+        /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+        /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+        /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+        /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+        /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+        /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
+        /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+        /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+        /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+        /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="isLocked">Optional filter by items that are locked.</param>
+        /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+        /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+        /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+        /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+        /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+        /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+        /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+        /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+        /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+        /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
+        [HttpGet("/Trailers")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
+            [FromQuery] Guid userId,
+            [FromQuery] string? maxOfficialRating,
+            [FromQuery] bool? hasThemeSong,
+            [FromQuery] bool? hasThemeVideo,
+            [FromQuery] bool? hasSubtitles,
+            [FromQuery] bool? hasSpecialFeature,
+            [FromQuery] bool? hasTrailer,
+            [FromQuery] string? adjacentTo,
+            [FromQuery] int? parentIndexNumber,
+            [FromQuery] bool? hasParentalRating,
+            [FromQuery] bool? isHd,
+            [FromQuery] bool? is4K,
+            [FromQuery] string? locationTypes,
+            [FromQuery] string? excludeLocationTypes,
+            [FromQuery] bool? isMissing,
+            [FromQuery] bool? isUnaired,
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] double? minCriticRating,
+            [FromQuery] DateTime? minPremiereDate,
+            [FromQuery] DateTime? minDateLastSaved,
+            [FromQuery] DateTime? minDateLastSavedForUser,
+            [FromQuery] DateTime? maxPremiereDate,
+            [FromQuery] bool? hasOverview,
+            [FromQuery] bool? hasImdbId,
+            [FromQuery] bool? hasTmdbId,
+            [FromQuery] bool? hasTvdbId,
+            [FromQuery] string? excludeItemIds,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] bool? recursive,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? imageTypes,
+            [FromQuery] string? sortBy,
+            [FromQuery] bool? isPlayed,
+            [FromQuery] string? genres,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? artists,
+            [FromQuery] string? excludeArtistIds,
+            [FromQuery] string? artistIds,
+            [FromQuery] string? albumArtistIds,
+            [FromQuery] string? contributingArtistIds,
+            [FromQuery] string? albums,
+            [FromQuery] string? albumIds,
+            [FromQuery] string? ids,
+            [FromQuery] string? videoTypes,
+            [FromQuery] string? minOfficialRating,
+            [FromQuery] bool? isLocked,
+            [FromQuery] bool? isPlaceHolder,
+            [FromQuery] bool? hasOfficialRating,
+            [FromQuery] bool? collapseBoxSetItems,
+            [FromQuery] int? minWidth,
+            [FromQuery] int? minHeight,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] bool? is3D,
+            [FromQuery] string? seriesStatus,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
+            [FromQuery] string? studioIds,
+            [FromQuery] string? genreIds,
+            [FromQuery] bool enableTotalRecordCount = true,
+            [FromQuery] bool? enableImages = true)
+        {
+            var includeItemTypes = "Trailer";
+
+            return new ItemsController(
+                _userManager,
+                _libraryManager,
+                _localizationManager,
+                _dtoService,
+                _logger)
+                .GetItems(
+                    userId,
+                    maxOfficialRating,
+                    hasThemeSong,
+                    hasThemeVideo,
+                    hasSubtitles,
+                    hasSpecialFeature,
+                    hasTrailer,
+                    adjacentTo,
+                    parentIndexNumber,
+                    hasParentalRating,
+                    isHd,
+                    is4K,
+                    locationTypes,
+                    excludeLocationTypes,
+                    isMissing,
+                    isUnaired,
+                    minCommunityRating,
+                    minCriticRating,
+                    minPremiereDate,
+                    minDateLastSaved,
+                    minDateLastSavedForUser,
+                    maxPremiereDate,
+                    hasOverview,
+                    hasImdbId,
+                    hasTmdbId,
+                    hasTvdbId,
+                    excludeItemIds,
+                    startIndex,
+                    limit,
+                    recursive,
+                    searchTerm,
+                    sortOrder,
+                    parentId,
+                    fields,
+                    excludeItemTypes,
+                    includeItemTypes,
+                    filters,
+                    isFavorite,
+                    mediaTypes,
+                    imageTypes,
+                    sortBy,
+                    isPlayed,
+                    genres,
+                    officialRatings,
+                    tags,
+                    years,
+                    enableUserData,
+                    imageTypeLimit,
+                    enableImageTypes,
+                    person,
+                    personIds,
+                    personTypes,
+                    studios,
+                    artists,
+                    excludeArtistIds,
+                    artistIds,
+                    albumArtistIds,
+                    contributingArtistIds,
+                    albums,
+                    albumIds,
+                    ids,
+                    videoTypes,
+                    minOfficialRating,
+                    isLocked,
+                    isPlaceHolder,
+                    hasOfficialRating,
+                    collapseBoxSetItems,
+                    minWidth,
+                    minHeight,
+                    maxWidth,
+                    maxHeight,
+                    is3D,
+                    seriesStatus,
+                    nameStartsWithOrGreater,
+                    nameStartsWith,
+                    nameLessThan,
+                    studioIds,
+                    genreIds,
+                    enableTotalRecordCount,
+                    enableImages);
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs
deleted file mode 100644
index 0b53342359..0000000000
--- a/MediaBrowser.Api/Movies/TrailersService.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
-    [Route("/Trailers", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
-    public class Getrailers : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    /// <summary>
-    /// Class TrailersService
-    /// </summary>
-    [Authenticated]
-    public class TrailersService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// The logger for the created <see cref="ItemsService"/> instances.
-        /// </summary>
-        private readonly ILogger<ItemsService> _logger;
-
-        private readonly IDtoService _dtoService;
-        private readonly ILocalizationManager _localizationManager;
-        private readonly IJsonSerializer _json;
-        private readonly IAuthorizationContext _authContext;
-
-        public TrailersService(
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            ILocalizationManager localizationManager,
-            IJsonSerializer json,
-            IAuthorizationContext authContext)
-            : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _localizationManager = localizationManager;
-            _json = json;
-            _authContext = authContext;
-            _logger = loggerFactory.CreateLogger<ItemsService>();
-        }
-
-        public object Get(Getrailers request)
-        {
-            var json = _json.SerializeToString(request);
-            var getItems = _json.DeserializeFromString<GetItems>(json);
-
-            getItems.IncludeItemTypes = "Trailer";
-
-            return new ItemsService(
-                _logger,
-                ServerConfigurationManager,
-                ResultFactory,
-                _userManager,
-                _libraryManager,
-                _localizationManager,
-                _dtoService,
-                _authContext)
-            {
-                Request = Request,
-
-            }.Get(getItems);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
deleted file mode 100644
index 04c3b97c07..0000000000
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ /dev/null
@@ -1,511 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetItems
-    /// </summary>
-    public class GetItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    public class GetResumeItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    /// <summary>
-    /// Class ItemsService
-    /// </summary>
-    [Authenticated]
-    public class ItemsService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILocalizationManager _localization;
-
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ItemsService" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="localization">The localization.</param>
-        /// <param name="dtoService">The dto service.</param>
-        public ItemsService(
-            ILogger<ItemsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            ILocalizationManager localization,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _localization = localization;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Get(GetResumeItems request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
-            var options = GetDtoOptions(_authContext, request);
-
-            var ancestorIds = Array.Empty<Guid>();
-
-            var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
-            if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
-            {
-                ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
-                    .Where(i => i is Folder)
-                    .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
-                    .Select(i => i.Id)
-                    .ToArray();
-            }
-
-            var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
-            {
-                OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
-                IsResumable = true,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = options,
-                MediaTypes = request.GetMediaTypes(),
-                IsVirtualItem = false,
-                CollapseBoxSetItems = false,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                AncestorIds = ancestorIds,
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                ExcludeItemTypes = request.GetExcludeItemTypes(),
-                SearchTerm = request.SearchTerm
-            });
-
-            var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                StartIndex = request.StartIndex.GetValueOrDefault(),
-                TotalRecordCount = itemsResult.TotalRecordCount,
-                Items = returnItems
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItems request)
-        {
-            if (request == null)
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            var result = GetItems(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        private QueryResult<BaseItemDto> GetItems(GetItems request)
-        {
-            var user = request.UserId == Guid.Empty ? null : _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = GetQueryResult(request, dtoOptions, user);
-
-            if (result == null)
-            {
-                throw new InvalidOperationException("GetItemsToSerialize returned null");
-            }
-
-            if (result.Items == null)
-            {
-                throw new InvalidOperationException("GetItemsToSerialize result.Items returned null");
-            }
-
-            var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                StartIndex = request.StartIndex.GetValueOrDefault(),
-                TotalRecordCount = result.TotalRecordCount,
-                Items = dtoList
-            };
-        }
-
-        /// <summary>
-        /// Gets the items to serialize.
-        /// </summary>
-        private QueryResult<BaseItem> GetQueryResult(GetItems request, DtoOptions dtoOptions, User user)
-        {
-            if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
-            {
-                request.ParentId = null;
-            }
-
-            BaseItem item = null;
-
-            if (!string.IsNullOrEmpty(request.ParentId))
-            {
-                item = _libraryManager.GetItemById(request.ParentId);
-            }
-
-            if (item == null)
-            {
-                item = _libraryManager.GetUserRootFolder();
-            }
-
-            if (!(item is Folder folder))
-            {
-                folder = _libraryManager.GetUserRootFolder();
-            }
-
-            if (folder is IHasCollectionType hasCollectionType
-                && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
-            {
-                request.Recursive = true;
-                request.IncludeItemTypes = "Playlist";
-            }
-
-            bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
-                    // Assume all folders inside an EnabledChannel are enabled
-                    || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
-
-            var collectionFolders = _libraryManager.GetCollectionFolders(item);
-            foreach (var collectionFolder in collectionFolders)
-            {
-                if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
-                    collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
-                    StringComparer.OrdinalIgnoreCase))
-                {
-                    isInEnabledFolder = true;
-                }
-            }
-
-            if (!(item is UserRootFolder)
-                && !isInEnabledFolder
-                && !user.HasPermission(PermissionKind.EnableAllFolders)
-                && !user.HasPermission(PermissionKind.EnableAllChannels))
-            {
-                Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
-                return new QueryResult<BaseItem>
-                {
-                    Items = Array.Empty<BaseItem>(),
-                    TotalRecordCount = 0,
-                    StartIndex = 0
-                };
-            }
-
-            if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
-            {
-                return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
-            }
-
-            var itemsArray = folder.GetChildren(user, true);
-            return new QueryResult<BaseItem>
-            {
-                Items = itemsArray,
-                TotalRecordCount = itemsArray.Count,
-                StartIndex = 0
-            };
-        }
-
-        private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, User user)
-        {
-            var query = new InternalItemsQuery(user)
-            {
-                IsPlayed = request.IsPlayed,
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                ExcludeItemTypes = request.GetExcludeItemTypes(),
-                Recursive = request.Recursive,
-                OrderBy = request.GetOrderBy(),
-
-                IsFavorite = request.IsFavorite,
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                IsMissing = request.IsMissing,
-                IsUnaired = request.IsUnaired,
-                CollapseBoxSetItems = request.CollapseBoxSetItems,
-                NameLessThan = request.NameLessThan,
-                NameStartsWith = request.NameStartsWith,
-                NameStartsWithOrGreater = request.NameStartsWithOrGreater,
-                HasImdbId = request.HasImdbId,
-                IsPlaceHolder = request.IsPlaceHolder,
-                IsLocked = request.IsLocked,
-                MinWidth = request.MinWidth,
-                MinHeight = request.MinHeight,
-                MaxWidth = request.MaxWidth,
-                MaxHeight = request.MaxHeight,
-                Is3D = request.Is3D,
-                HasTvdbId = request.HasTvdbId,
-                HasTmdbId = request.HasTmdbId,
-                HasOverview = request.HasOverview,
-                HasOfficialRating = request.HasOfficialRating,
-                HasParentalRating = request.HasParentalRating,
-                HasSpecialFeature = request.HasSpecialFeature,
-                HasSubtitles = request.HasSubtitles,
-                HasThemeSong = request.HasThemeSong,
-                HasThemeVideo = request.HasThemeVideo,
-                HasTrailer = request.HasTrailer,
-                IsHD = request.IsHD,
-                Is4K = request.Is4K,
-                Tags = request.GetTags(),
-                OfficialRatings = request.GetOfficialRatings(),
-                Genres = request.GetGenres(),
-                ArtistIds = GetGuids(request.ArtistIds),
-                AlbumArtistIds = GetGuids(request.AlbumArtistIds),
-                ContributingArtistIds = GetGuids(request.ContributingArtistIds),
-                GenreIds = GetGuids(request.GenreIds),
-                StudioIds = GetGuids(request.StudioIds),
-                Person = request.Person,
-                PersonIds = GetGuids(request.PersonIds),
-                PersonTypes = request.GetPersonTypes(),
-                Years = request.GetYears(),
-                ImageTypes = request.GetImageTypes(),
-                VideoTypes = request.GetVideoTypes(),
-                AdjacentTo = request.AdjacentTo,
-                ItemIds = GetGuids(request.Ids),
-                MinCommunityRating = request.MinCommunityRating,
-                MinCriticRating = request.MinCriticRating,
-                ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId),
-                ParentIndexNumber = request.ParentIndexNumber,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                ExcludeItemIds = GetGuids(request.ExcludeItemIds),
-                DtoOptions = dtoOptions,
-                SearchTerm = request.SearchTerm
-            };
-
-            if (!string.IsNullOrWhiteSpace(request.Ids) || !string.IsNullOrWhiteSpace(request.SearchTerm))
-            {
-                query.CollapseBoxSetItems = false;
-            }
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(request.MinDateLastSaved))
-            {
-                query.MinDateLastSaved = DateTime.Parse(request.MinDateLastSaved, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinDateLastSavedForUser))
-            {
-                query.MinDateLastSavedForUser = DateTime.Parse(request.MinDateLastSavedForUser, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinPremiereDate))
-            {
-                query.MinPremiereDate = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxPremiereDate))
-            {
-                query.MaxPremiereDate = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            // Filter by Series Status
-            if (!string.IsNullOrEmpty(request.SeriesStatus))
-            {
-                query.SeriesStatuses = request.SeriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
-            }
-
-            // ExcludeLocationTypes
-            if (!string.IsNullOrEmpty(request.ExcludeLocationTypes))
-            {
-                var excludeLocationTypes = request.ExcludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray();
-                if (excludeLocationTypes.Contains(LocationType.Virtual))
-                {
-                    query.IsVirtualItem = false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(request.LocationTypes))
-            {
-                var requestedLocationTypes =
-                    request.LocationTypes.Split(',');
-
-                if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
-                {
-                    query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
-                }
-            }
-
-            // Min official rating
-            if (!string.IsNullOrWhiteSpace(request.MinOfficialRating))
-            {
-                query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating);
-            }
-
-            // Max official rating
-            if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating))
-            {
-                query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating);
-            }
-
-            // Artists
-            if (!string.IsNullOrEmpty(request.Artists))
-            {
-                query.ArtistIds = request.Artists.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return _libraryManager.GetArtist(i, new DtoOptions(false));
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrWhiteSpace(request.ExcludeArtistIds))
-            {
-                query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.AlbumIds))
-            {
-                query.AlbumIds = GetGuids(request.AlbumIds);
-            }
-
-            // Albums
-            if (!string.IsNullOrEmpty(request.Albums))
-            {
-                query.AlbumIds = request.Albums.Split('|').SelectMany(i =>
-                {
-                    return _libraryManager.GetItemIds(new InternalItemsQuery
-                    {
-                        IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
-                        Name = i,
-                        Limit = 1
-                    });
-                }).ToArray();
-            }
-
-            // Studios
-            if (!string.IsNullOrEmpty(request.Studios))
-            {
-                query.StudioIds = request.Studios.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return _libraryManager.GetStudio(i);
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            // Apply default sorting if none requested
-            if (query.OrderBy.Count == 0)
-            {
-                // Albums by artist
-                if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
-                {
-                    query.OrderBy = new[]
-                    {
-                        new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending),
-                        new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
-                    };
-                }
-            }
-
-            return query;
-        }
-    }
-
-    /// <summary>
-    /// Class DateCreatedComparer
-    /// </summary>
-    public class DateCreatedComparer : IComparer<BaseItem>
-    {
-        /// <summary>
-        /// Compares the specified x.
-        /// </summary>
-        /// <param name="x">The x.</param>
-        /// <param name="y">The y.</param>
-        /// <returns>System.Int32.</returns>
-        public int Compare(BaseItem x, BaseItem y)
-        {
-            return x.DateCreated.CompareTo(y.DateCreated);
-        }
-    }
-}

From 68cc075ddaf1f604182ad21d7d00ee9940522c4e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 3 Jul 2020 19:04:45 +0200
Subject: [PATCH 287/463] Update LiveTvController.cs

---
 Jellyfin.Api/Controllers/LiveTvController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 580bf849a6..325837ce38 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -731,7 +731,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">Optional. Attach user data.</param>
         /// <response code="200">Program returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
-        [HttpGet("Programs/{programId{")]
+        [HttpGet("Programs/{programId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<BaseItemDto>> GetProgram(

From 94ccb3ee98b9b9f9988bd617086f848304c1e1da Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 4 Jul 2020 18:50:16 +0200
Subject: [PATCH 288/463] Move GenresService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/GenresController.cs  | 317 ++++++++++++++++++
 MediaBrowser.Api/UserLibrary/GenresService.cs | 140 --------
 2 files changed, 317 insertions(+), 140 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/GenresController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/GenresService.cs

diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
new file mode 100644
index 0000000000..9b15bdb771
--- /dev/null
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -0,0 +1,317 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The genres controller.
+    /// </summary>
+    public class GenresController : BaseJellyfinApiController
+    {
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GenresController"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        public GenresController(
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IDtoService dtoService)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets all genres from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">The search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
+        /// <response code="200">Genres returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
+        [HttpGet]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetGenres(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|')
+                    .Select(i =>
+                    {
+                        try
+                        {
+                            return _libraryManager.GetStudio(i);
+                        }
+                        catch
+                        {
+                            return null;
+                        }
+                    }).Where(i => i != null)
+                    .Select(i => i!.Id)
+                    .ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = new QueryResult<(BaseItem, ItemCounts)>();
+
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, counts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = counts.ItemCount;
+                    dto.ProgramCount = counts.ProgramCount;
+                    dto.SeriesCount = counts.SeriesCount;
+                    dto.EpisodeCount = counts.EpisodeCount;
+                    dto.MovieCount = counts.MovieCount;
+                    dto.TrailerCount = counts.TrailerCount;
+                    dto.AlbumCount = counts.AlbumCount;
+                    dto.SongCount = counts.SongCount;
+                    dto.ArtistCount = counts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets a genre, by name.
+        /// </summary>
+        /// <param name="genreName">The genre name.</param>
+        /// <param name="userId">The user id.</param>
+        /// <response code="200">Genres returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the genre.</returns>
+        [HttpGet("{genreName}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid userId)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddClientFields(Request);
+
+            Genre item = new Genre();
+            if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions);
+
+                if (result != null)
+                {
+                    item = result;
+                }
+            }
+            else
+            {
+                item = _libraryManager.GetGenre(genreName);
+            }
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+
+        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+            where T : BaseItem, new()
+        {
+            var result = libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '&'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            result ??= libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '/'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            result ??= libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '?'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            return result;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs
deleted file mode 100644
index 1fa272a5f7..0000000000
--- a/MediaBrowser.Api/UserLibrary/GenresService.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetGenres
-    /// </summary>
-    [Route("/Genres", "GET", Summary = "Gets all genres from a given item, folder, or the entire library")]
-    public class GetGenres : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetGenre
-    /// </summary>
-    [Route("/Genres/{Name}", "GET", Summary = "Gets a genre, by name")]
-    public class GetGenre : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GenresService
-    /// </summary>
-    [Authenticated]
-    public class GenresService : BaseItemsByNameService<Genre>
-    {
-        public GenresService(
-            ILogger<GenresService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGenre request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetGenre request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetGenre(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGenres request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            var viewType = GetParentItemViewType(request);
-
-            if (string.Equals(viewType, CollectionType.Music) || string.Equals(viewType, CollectionType.MusicVideos))
-            {
-                return LibraryManager.GetMusicGenres(query);
-            }
-
-            return LibraryManager.GetGenres(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

From 589add16548e407b5b709e8febca03ee666f5e8e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 4 Jul 2020 19:05:42 +0200
Subject: [PATCH 289/463] Change nullable behavior to fix web client

---
 Jellyfin.Api/Controllers/ItemsController.cs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 161d30e876..639891d8ef 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -288,7 +288,7 @@ namespace Jellyfin.Api.Controllers
                     MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
                     IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
                     ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
-                    Recursive = recursive!.Value,
+                    Recursive = recursive ?? false,
                     OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                     IsFavorite = isFavorite,
                     Limit = limit,
@@ -343,10 +343,10 @@ namespace Jellyfin.Api.Controllers
                     ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
                     DtoOptions = dtoOptions,
                     SearchTerm = searchTerm,
-                    MinDateLastSaved = minDateLastSaved!.Value.ToUniversalTime(),
-                    MinDateLastSavedForUser = minDateLastSavedForUser!.Value.ToUniversalTime(),
-                    MinPremiereDate = minPremiereDate!.Value.ToUniversalTime(),
-                    MaxPremiereDate = maxPremiereDate!.Value.ToUniversalTime(),
+                    MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
+                    MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
+                    MinPremiereDate = minPremiereDate?.ToUniversalTime(),
+                    MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
                 };
 
                 if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))

From 57fab9035f642c78124fbce799e8ae72820ac834 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 5 Jul 2020 11:04:14 +0200
Subject: [PATCH 290/463] Move MusicGenresService to Jellyfin.Api

---
 .../Controllers/MusicGenresController.cs      | 311 ++++++++++++++++++
 .../UserLibrary/MusicGenresService.cs         | 124 -------
 2 files changed, 311 insertions(+), 124 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/MusicGenresController.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/MusicGenresService.cs

diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
new file mode 100644
index 0000000000..c1a59aa836
--- /dev/null
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The music genres controller.
+    /// </summary>
+    [Route("/MusicGenres")]
+    public class MusicGenresController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDtoService _dtoService;
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MusicGenresController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+        public MusicGenresController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDtoService dtoService)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dtoService = dtoService;
+        }
+
+        /// <summary>
+        /// Gets all music genres from a given item, folder, or the entire library.
+        /// </summary>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="searchTerm">The search term.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+        /// <param name="userId">User id.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
+        /// <response code="200">Music genres returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
+        [HttpGet]
+        public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] string searchTerm,
+            [FromQuery] string parentId,
+            [FromQuery] string fields,
+            [FromQuery] string excludeItemTypes,
+            [FromQuery] string includeItemTypes,
+            [FromQuery] string filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string mediaTypes,
+            [FromQuery] string genres,
+            [FromQuery] string genreIds,
+            [FromQuery] string officialRatings,
+            [FromQuery] string tags,
+            [FromQuery] string years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery] string enableImageTypes,
+            [FromQuery] string person,
+            [FromQuery] string personIds,
+            [FromQuery] string personTypes,
+            [FromQuery] string studios,
+            [FromQuery] string studioIds,
+            [FromQuery] Guid userId,
+            [FromQuery] string nameStartsWithOrGreater,
+            [FromQuery] string nameStartsWith,
+            [FromQuery] string nameLessThan,
+            [FromQuery] bool? enableImages = true,
+            [FromQuery] bool enableTotalRecordCount = true)
+        {
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request)
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+            User? user = null;
+            BaseItem parentItem;
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                user = _userManager.GetUserById(userId);
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
+            }
+            else
+            {
+                parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
+            }
+
+            var query = new InternalItemsQuery(user)
+            {
+                ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+                IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+                MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+                StartIndex = startIndex,
+                Limit = limit,
+                IsFavorite = isFavorite,
+                NameLessThan = nameLessThan,
+                NameStartsWith = nameStartsWith,
+                NameStartsWithOrGreater = nameStartsWithOrGreater,
+                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,
+                PersonIds = RequestHelpers.GetGuids(personIds),
+                PersonTypes = RequestHelpers.Split(personTypes, ',', true),
+                Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
+                MinCommunityRating = minCommunityRating,
+                DtoOptions = dtoOptions,
+                SearchTerm = searchTerm,
+                EnableTotalRecordCount = enableTotalRecordCount
+            };
+
+            if (!string.IsNullOrWhiteSpace(parentId))
+            {
+                if (parentItem is Folder)
+                {
+                    query.AncestorIds = new[] { new Guid(parentId) };
+                }
+                else
+                {
+                    query.ItemIds = new[] { new Guid(parentId) };
+                }
+            }
+
+            // Studios
+            if (!string.IsNullOrEmpty(studios))
+            {
+                query.StudioIds = studios.Split('|')
+                    .Select(i =>
+                    {
+                        try
+                        {
+                            return _libraryManager.GetStudio(i);
+                        }
+                        catch
+                        {
+                            return null;
+                        }
+                    }).Where(i => i != null)
+                    .Select(i => i!.Id)
+                    .ToArray();
+            }
+
+            foreach (var filter in RequestHelpers.GetFilters(filters))
+            {
+                switch (filter)
+                {
+                    case ItemFilter.Dislikes:
+                        query.IsLiked = false;
+                        break;
+                    case ItemFilter.IsFavorite:
+                        query.IsFavorite = true;
+                        break;
+                    case ItemFilter.IsFavoriteOrLikes:
+                        query.IsFavoriteOrLiked = true;
+                        break;
+                    case ItemFilter.IsFolder:
+                        query.IsFolder = true;
+                        break;
+                    case ItemFilter.IsNotFolder:
+                        query.IsFolder = false;
+                        break;
+                    case ItemFilter.IsPlayed:
+                        query.IsPlayed = true;
+                        break;
+                    case ItemFilter.IsResumable:
+                        query.IsResumable = true;
+                        break;
+                    case ItemFilter.IsUnplayed:
+                        query.IsPlayed = false;
+                        break;
+                    case ItemFilter.Likes:
+                        query.IsLiked = true;
+                        break;
+                }
+            }
+
+            var result = _libraryManager.GetMusicGenres(query);
+
+            var dtos = result.Items.Select(i =>
+            {
+                var (baseItem, counts) = i;
+                var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+
+                if (!string.IsNullOrWhiteSpace(includeItemTypes))
+                {
+                    dto.ChildCount = counts.ItemCount;
+                    dto.ProgramCount = counts.ProgramCount;
+                    dto.SeriesCount = counts.SeriesCount;
+                    dto.EpisodeCount = counts.EpisodeCount;
+                    dto.MovieCount = counts.MovieCount;
+                    dto.TrailerCount = counts.TrailerCount;
+                    dto.AlbumCount = counts.AlbumCount;
+                    dto.SongCount = counts.SongCount;
+                    dto.ArtistCount = counts.ArtistCount;
+                }
+
+                return dto;
+            });
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = dtos.ToArray(),
+                TotalRecordCount = result.TotalRecordCount
+            };
+        }
+
+        /// <summary>
+        /// Gets a music genre, by name.
+        /// </summary>
+        /// <param name="genreName">The genre name.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns>
+        [HttpGet("{genreName}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<BaseItemDto> GetMusicGenre([FromRoute] string genreName, [FromQuery] Guid userId)
+        {
+            var dtoOptions = new DtoOptions().AddClientFields(Request);
+
+            MusicGenre item;
+
+            if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions);
+            }
+            else
+            {
+                item = _libraryManager.GetMusicGenre(genreName);
+            }
+
+            if (!userId.Equals(Guid.Empty))
+            {
+                var user = _userManager.GetUserById(userId);
+
+                return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+            }
+
+            return _dtoService.GetBaseItemDto(item, dtoOptions);
+        }
+
+        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+            where T : BaseItem, new()
+        {
+            var result = libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '&'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            result ??= libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '/'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            result ??= libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Name = name.Replace(BaseItem.SlugChar, '?'),
+                IncludeItemTypes = new[] { typeof(T).Name },
+                DtoOptions = dtoOptions
+            }).OfType<T>().FirstOrDefault();
+
+            return result;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs
deleted file mode 100644
index e9caca14aa..0000000000
--- a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    [Route("/MusicGenres", "GET", Summary = "Gets all music genres from a given item, folder, or the entire library")]
-    public class GetMusicGenres : GetItemsByName
-    {
-    }
-
-    [Route("/MusicGenres/{Name}", "GET", Summary = "Gets a music genre, by name")]
-    public class GetMusicGenre : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Authenticated]
-    public class MusicGenresService : BaseItemsByNameService<MusicGenre>
-    {
-        public MusicGenresService(
-            ILogger<MusicGenresService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetMusicGenre request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetMusicGenre request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetMusicGenre(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetMusicGenres request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return LibraryManager.GetMusicGenres(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

From 5a74a7d3c7696ce419939b0cb2deac1fc45658c7 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 5 Jul 2020 11:10:09 +0200
Subject: [PATCH 291/463] Add additional userId query parameter

---
 Jellyfin.Api/Controllers/ItemsController.cs | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 639891d8ef..4c6c43b0a4 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -57,7 +57,8 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets items based on a query.
         /// </summary>
-        /// <param name="userId">The user id.</param>
+        /// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
+        /// <param name="userId">The user id supplied as query parameter.</param>
         /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
         /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
         /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
@@ -142,7 +143,8 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Users/{userId}/Items")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
-            [FromRoute] Guid userId,
+            [FromRoute] Guid uId,
+            [FromQuery] Guid userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
             [FromQuery] bool? hasThemeVideo,
@@ -223,6 +225,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool? enableImages = true)
         {
+            // use user id route parameter over query parameter
+            userId = (uId != null) ? uId : userId;
+
             var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)

From 068725cdedecc88bcde9f18c68b9b1e5c0f6e569 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 6 Jul 2020 08:02:23 -0600
Subject: [PATCH 292/463] Fix documentation and authorize attribute

---
 Jellyfin.Api/Controllers/ChannelsController.cs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index c3e29323e3..a293a78a02 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Controller.Channels;
@@ -21,7 +22,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Channels Controller.
     /// </summary>
-    [Authorize]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ChannelsController : BaseJellyfinApiController
     {
         private readonly IChannelManager _channelManager;
@@ -107,7 +108,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <response code="200">Channel items returned.</response>
-        /// <returns>Channel items.</returns>
+        /// <returns>
+        /// A <see cref="Task"/> representing the request to get the channel items.
+        /// The task result contains an <see cref="OkResult"/> containing the channel items.
+        /// </returns>
         [HttpGet("{channelId}/Items")]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
             [FromRoute] Guid channelId,

From 87f3326ffbc2dd807b663bac7551141bce9c35fe Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 6 Jul 2020 17:36:24 +0200
Subject: [PATCH 293/463] Add authorization attribute

---
 Jellyfin.Api/Controllers/GenresController.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 9b15bdb771..2cd3de76ef 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -18,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The genres controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class GenresController : BaseJellyfinApiController
     {
         private readonly IUserManager _userManager;

From c71234f4d902b068e6b6afb116d17cc14ca7495d Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 6 Jul 2020 17:39:45 +0200
Subject: [PATCH 294/463] Add authorization attribute

---
 Jellyfin.Api/Controllers/MusicGenresController.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index c1a59aa836..9ac74f199e 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -10,6 +11,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -18,7 +20,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The music genres controller.
     /// </summary>
-    [Route("/MusicGenres")]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class MusicGenresController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;

From 95ea9dad0012557afe512a338055fab30d17fb22 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 6 Jul 2020 17:41:02 +0200
Subject: [PATCH 295/463] Change route parameter name

---
 Jellyfin.Api/Controllers/ItemsController.cs    | 2 +-
 Jellyfin.Api/Controllers/TrailersController.cs | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 4c6c43b0a4..e1dd4af101 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional, include image information in output.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
         [HttpGet("/Items")]
-        [HttpGet("/Users/{userId}/Items")]
+        [HttpGet("/Users/{uId}/Items")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
             [FromRoute] Guid uId,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 39931f1976..bd65abd509 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -221,6 +221,7 @@ namespace Jellyfin.Api.Controllers
                 _dtoService,
                 _logger)
                 .GetItems(
+                    userId,
                     userId,
                     maxOfficialRating,
                     hasThemeSong,

From c2ae0b492c61a0e34e84c7241c3dbc4e4341ac3d Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Mon, 6 Jul 2020 17:43:34 +0200
Subject: [PATCH 296/463] Add missing using

---
 Jellyfin.Api/Controllers/GenresController.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 2cd3de76ef..d57989a8a4 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Genre = MediaBrowser.Controller.Entities.Genre;

From 1bf131c1095f1b2d99e6267fa175aed4f7db88fc Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 6 Jul 2020 10:02:16 -0600
Subject: [PATCH 297/463] remove duplicate functions

---
 Jellyfin.Api/Helpers/RequestHelpers.cs | 51 --------------------------
 1 file changed, 51 deletions(-)

diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 070711681b..299c7d4aaa 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -147,57 +147,6 @@ namespace Jellyfin.Api.Helpers
                 .ToArray();
         }
 
-        /// <summary>
-        /// Get orderby.
-        /// </summary>
-        /// <param name="sortBy">Sort by.</param>
-        /// <param name="requestedSortOrder">Sort order.</param>
-        /// <returns>Resulting order by.</returns>
-        internal static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
-        {
-            if (string.IsNullOrEmpty(sortBy))
-            {
-                return Array.Empty<ValueTuple<string, SortOrder>>();
-            }
-
-            var vals = sortBy.Split(',');
-            if (string.IsNullOrWhiteSpace(requestedSortOrder))
-            {
-                requestedSortOrder = "Ascending";
-            }
-
-            var sortOrders = requestedSortOrder.Split(',');
-
-            var result = new ValueTuple<string, SortOrder>[vals.Length];
-
-            for (var i = 0; i < vals.Length; i++)
-            {
-                var sortOrderIndex = sortOrders.Length > i ? i : 0;
-
-                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
-                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
-                    ? SortOrder.Descending
-                    : SortOrder.Ascending;
-
-                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <param name="filters">The filter string.</param>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        internal static ItemFilter[] GetFilters(string filters)
-        {
-            return string.IsNullOrEmpty(filters)
-                ? Array.Empty<ItemFilter>()
-                : Split(filters, ',', true)
-                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
-        }
-
         /// <summary>
         /// Gets the item fields.
         /// </summary>

From 5d34b07d1ff7239c7961381fc71559d377e7a96b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 7 Jul 2020 09:10:51 -0600
Subject: [PATCH 298/463] Make query parameters nullable or set default value

---
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs |   2 +-
 Jellyfin.Api/Controllers/AlbumsController.cs  |   4 +-
 Jellyfin.Api/Controllers/ArtistsController.cs | 102 ++++++------
 .../Controllers/ChannelsController.cs         |  22 +--
 .../Controllers/CollectionController.cs       |   6 +-
 Jellyfin.Api/Controllers/FilterController.cs  |  12 +-
 Jellyfin.Api/Controllers/GenresController.cs  |  54 +++---
 .../Controllers/InstantMixController.cs       |  44 +++--
 Jellyfin.Api/Controllers/ItemsController.cs   |  10 +-
 Jellyfin.Api/Controllers/LibraryController.cs |  56 ++++---
 .../Controllers/LibraryStructureController.cs |  20 +--
 Jellyfin.Api/Controllers/LiveTvController.cs  | 156 ++++++++++--------
 .../Controllers/MediaInfoController.cs        |  40 ++---
 Jellyfin.Api/Controllers/MoviesController.cs  |  26 +--
 .../Controllers/MusicGenresController.cs      |  54 +++---
 Jellyfin.Api/Controllers/PersonsController.cs |  54 +++---
 .../Controllers/PlaylistsController.cs        |   4 +-
 .../Controllers/PlaystateController.cs        |  32 ++--
 .../Controllers/RemoteImageController.cs      |   2 +-
 Jellyfin.Api/Controllers/SearchController.cs  |   4 +-
 Jellyfin.Api/Controllers/SessionController.cs |  12 +-
 Jellyfin.Api/Controllers/StudiosController.cs |  54 +++---
 .../Controllers/SubtitleController.cs         |   4 +-
 .../Controllers/SuggestionsController.cs      |   6 +-
 .../Controllers/TrailersController.cs         |   2 +-
 Jellyfin.Api/Controllers/TvShowsController.cs |  26 ++-
 .../Controllers/UserLibraryController.cs      |   6 +-
 .../Controllers/UserViewsController.cs        |   6 +-
 Jellyfin.Api/Controllers/VideosController.cs  |   6 +-
 Jellyfin.Api/Controllers/YearsController.cs   |  12 +-
 Jellyfin.Api/Helpers/SimilarItemsHelper.cs    |  12 +-
 31 files changed, 442 insertions(+), 408 deletions(-)

diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 50b6468db4..9fde175d0b 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Auth
         {
             // Ensure claim has userId.
             var userId = ClaimHelpers.GetUserId(claimsPrincipal);
-            if (userId == null)
+            if (!userId.HasValue)
             {
                 return false;
             }
diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 70315b0a33..01ba7fc326 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
             [FromRoute] string albumId,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? excludeArtistIds,
             [FromQuery] int? limit)
         {
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
             [FromRoute] string artistId,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? excludeArtistIds,
             [FromQuery] int? limit)
         {
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 6b2084170e..d390214465 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -83,31 +83,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -119,9 +119,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -292,31 +292,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -328,9 +328,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -469,15 +469,15 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the artist.</returns>
         [HttpGet("{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid? userId)
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
 
             var item = _libraryManager.GetArtist(name, dtoOptions);
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
 
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index a293a78a02..bdd7dfd967 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetChannels(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] bool? supportsLatestItems,
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
             {
                 Limit = limit,
                 StartIndex = startIndex,
-                UserId = userId,
+                UserId = userId ?? Guid.Empty,
                 SupportsLatestItems = supportsLatestItems,
                 SupportsMediaDeletion = supportsMediaDeletion,
                 IsFavorite = isFavorite
@@ -124,9 +124,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? sortBy,
             [FromQuery] string? fields)
         {
-            var user = userId == null
-                ? null
-                : _userManager.GetUserById(userId.Value);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var query = new InternalItemsQuery(user)
             {
@@ -195,13 +195,13 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string filters,
-            [FromQuery] string fields,
-            [FromQuery] string channelIds)
+            [FromQuery] string? filters,
+            [FromQuery] string? fields,
+            [FromQuery] string? channelIds)
         {
-            var user = userId == null || userId == Guid.Empty
-                ? null
-                : _userManager.GetUserById(userId.Value);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var query = new InternalItemsQuery(user)
             {
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 7ff98b2513..6f78a7d844 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -44,8 +44,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="name">The name of the collection.</param>
         /// <param name="ids">Item Ids to add to the collection.</param>
-        /// <param name="isLocked">Whether or not to lock the new collection.</param>
         /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
+        /// <param name="isLocked">Whether or not to lock the new collection.</param>
         /// <response code="200">Collection created.</response>
         /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
         [HttpPost]
@@ -53,8 +53,8 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<CollectionCreationResult> CreateCollection(
             [FromQuery] string? name,
             [FromQuery] string? ids,
-            [FromQuery] bool isLocked,
-            [FromQuery] Guid? parentId)
+            [FromQuery] Guid? parentId,
+            [FromQuery] bool isLocked = false)
         {
             var userId = _authContext.GetAuthorizationInfo(Request).UserId;
 
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 8a0a6ad866..288d4c5459 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -57,9 +57,9 @@ namespace Jellyfin.Api.Controllers
                 ? null
                 : _libraryManager.GetItemById(parentId);
 
-            var user = userId == null || userId == Guid.Empty
-                ? null
-                : _userManager.GetUserById(userId.Value);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
                 || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
@@ -152,9 +152,9 @@ namespace Jellyfin.Api.Controllers
                 ? null
                 : _libraryManager.GetItemById(parentId);
 
-            var user = userId == null || userId == Guid.Empty
-                ? null
-                : _userManager.GetUserById(userId.Value);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
                 || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index d57989a8a4..55ad712007 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -84,31 +84,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -120,9 +120,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -260,7 +260,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the genre.</returns>
         [HttpGet("{genreName}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
         {
             var dtoOptions = new DtoOptions()
                 .AddClientFields(Request);
@@ -280,9 +280,9 @@ namespace Jellyfin.Api.Controllers
                 item = _libraryManager.GetGenre(genreName);
             }
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
 
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 9d945fe2b0..bb980af3e7 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -63,7 +63,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -72,7 +72,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var item = _libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -98,7 +100,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -107,7 +109,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var album = _libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -133,7 +137,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -142,7 +146,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var playlist = (Playlist)_libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -168,7 +174,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
             [FromRoute] string? name,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -176,7 +182,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? imageTypeLimit,
             [FromQuery] string? enableImageTypes)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -202,7 +210,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -211,7 +219,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var item = _libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -237,7 +247,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -246,7 +256,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var item = _libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -272,7 +284,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
             [FromRoute] Guid id,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
             [FromQuery] bool? enableImages,
@@ -281,7 +293,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes)
         {
             var item = _libraryManager.GetItemById(id);
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
@@ -290,7 +304,7 @@ namespace Jellyfin.Api.Controllers
             return GetResult(items, user, limit, dtoOptions);
         }
 
-        private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User user, int? limit, DtoOptions dtoOptions)
+        private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
         {
             var list = items;
 
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index e1dd4af101..41fe47db10 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -143,8 +143,8 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Users/{uId}/Items")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
-            [FromRoute] Guid uId,
-            [FromQuery] Guid userId,
+            [FromRoute] Guid? uId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
             [FromQuery] bool? hasThemeVideo,
@@ -226,9 +226,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true)
         {
             // use user id route parameter over query parameter
-            userId = (uId != null) ? uId : userId;
+            userId = uId ?? userId;
 
-            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index f1106cda60..2466b2ac89 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -146,11 +146,11 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<ThemeMediaResult> GetThemeSongs(
             [FromRoute] Guid itemId,
-            [FromQuery] Guid userId,
-            [FromQuery] bool inheritFromParent)
+            [FromQuery] Guid? userId,
+            [FromQuery] bool inheritFromParent = false)
         {
-            var user = !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId)
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
                 : null;
 
             var item = itemId.Equals(Guid.Empty)
@@ -212,11 +212,11 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<ThemeMediaResult> GetThemeVideos(
             [FromRoute] Guid itemId,
-            [FromQuery] Guid userId,
-            [FromQuery] bool inheritFromParent)
+            [FromQuery] Guid? userId,
+            [FromQuery] bool inheritFromParent = false)
         {
-            var user = !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId)
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
                 : null;
 
             var item = itemId.Equals(Guid.Empty)
@@ -277,8 +277,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
             [FromRoute] Guid itemId,
-            [FromQuery] Guid userId,
-            [FromQuery] bool inheritFromParent)
+            [FromQuery] Guid? userId,
+            [FromQuery] bool inheritFromParent = false)
         {
             var themeSongs = GetThemeSongs(
                 itemId,
@@ -361,12 +361,14 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status401Unauthorized)]
-        public ActionResult DeleteItems([FromQuery] string ids)
+        public ActionResult DeleteItems([FromQuery] string? ids)
         {
-            var itemIds = string.IsNullOrWhiteSpace(ids)
-                ? Array.Empty<string>()
-                : RequestHelpers.Split(ids, ',', true);
+            if (string.IsNullOrEmpty(ids))
+            {
+                return NoContent();
+            }
 
+            var itemIds = RequestHelpers.Split(ids, ',', true);
             foreach (var i in itemIds)
             {
                 var item = _libraryManager.GetItemById(i);
@@ -403,12 +405,12 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ItemCounts> GetItemCounts(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] bool? isFavorite)
         {
-            var user = userId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var counts = new ItemCounts
             {
@@ -437,7 +439,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid? userId)
         {
             var item = _libraryManager.GetItemById(itemId);
 
@@ -448,8 +450,8 @@ namespace Jellyfin.Api.Controllers
 
             var baseItemDtos = new List<BaseItemDto>();
 
-            var user = !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId)
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
                 : null;
 
             var dtoOptions = new DtoOptions().AddClientFields(Request);
@@ -688,7 +690,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
             [FromQuery] string? excludeArtistIds,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields)
         {
@@ -737,7 +739,9 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Libraries/AvailableOptions")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo([FromQuery] string? libraryContentType, [FromQuery] bool isNewLibrary)
+        public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
+            [FromQuery] string? libraryContentType,
+            [FromQuery] bool isNewLibrary = false)
         {
             var result = new LibraryOptionsResultDto();
 
@@ -878,13 +882,15 @@ namespace Jellyfin.Api.Controllers
         private QueryResult<BaseItemDto> GetSimilarItemsResult(
             BaseItem item,
             string? excludeArtistIds,
-            Guid userId,
+            Guid? userId,
             int? limit,
             string? fields,
             string[] includeItemTypes,
             bool isMovie)
         {
-            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 0c91f84477..881d3f1923 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="name">The name of the virtual folder.</param>
         /// <param name="collectionType">The type of the collection.</param>
-        /// <param name="refreshLibrary">Whether to refresh the library.</param>
         /// <param name="paths">The paths of the virtual folder.</param>
         /// <param name="libraryOptions">The library options.</param>
+        /// <param name="refreshLibrary">Whether to refresh the library.</param>
         /// <response code="204">Folder added.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost]
@@ -74,9 +74,9 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> AddVirtualFolder(
             [FromQuery] string? name,
             [FromQuery] string? collectionType,
-            [FromQuery] bool refreshLibrary,
             [FromQuery] string[] paths,
-            [FromQuery] LibraryOptions libraryOptions)
+            [FromQuery] LibraryOptions? libraryOptions,
+            [FromQuery] bool refreshLibrary = false)
         {
             libraryOptions ??= new LibraryOptions();
 
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RemoveVirtualFolder(
             [FromQuery] string? name,
-            [FromQuery] bool refreshLibrary)
+            [FromQuery] bool refreshLibrary = false)
         {
             await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
             return NoContent();
@@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult RenameVirtualFolder(
             [FromQuery] string? name,
             [FromQuery] string? newName,
-            [FromQuery] bool refreshLibrary)
+            [FromQuery] bool refreshLibrary = false)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
@@ -207,8 +207,8 @@ namespace Jellyfin.Api.Controllers
         public ActionResult AddMediaPath(
             [FromQuery] string? name,
             [FromQuery] string? path,
-            [FromQuery] MediaPathInfo pathInfo,
-            [FromQuery] bool refreshLibrary)
+            [FromQuery] MediaPathInfo? pathInfo,
+            [FromQuery] bool refreshLibrary = false)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
@@ -257,7 +257,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateMediaPath(
             [FromQuery] string? name,
-            [FromQuery] MediaPathInfo pathInfo)
+            [FromQuery] MediaPathInfo? pathInfo)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
@@ -282,7 +282,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult RemoveMediaPath(
             [FromQuery] string? name,
             [FromQuery] string? path,
-            [FromQuery] bool refreshLibrary)
+            [FromQuery] bool refreshLibrary = false)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
@@ -328,7 +328,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateLibraryOptions(
             [FromQuery] string? id,
-            [FromQuery] LibraryOptions libraryOptions)
+            [FromQuery] LibraryOptions? libraryOptions)
         {
             var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
 
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 325837ce38..bc5446510a 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -113,7 +113,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
         /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
         /// <param name="isDisliked">Optional. Filter by channels that are disliked, or not.</param>
-        /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">"Optional. The image types to include in the output.</param>
@@ -121,6 +120,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="sortBy">Optional. Key to sort by.</param>
         /// <param name="sortOrder">Optional. Sort order.</param>
+        /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
         /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
         /// <response code="200">Available live tv channels returned.</response>
         /// <returns>
@@ -131,7 +131,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         public ActionResult<QueryResult<BaseItemDto>> GetChannels(
             [FromQuery] ChannelType? type,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSeries,
@@ -142,14 +142,14 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isLiked,
             [FromQuery] bool? isDisliked,
-            [FromQuery] bool enableFavoriteSorting,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string fields,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableUserData,
-            [FromQuery] string sortBy,
+            [FromQuery] string? sortBy,
             [FromQuery] SortOrder? sortOrder,
+            [FromQuery] bool enableFavoriteSorting = false,
             [FromQuery] bool addCurrentProgram = true)
         {
             var dtoOptions = new DtoOptions()
@@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
                 new LiveTvChannelQuery
                 {
                     ChannelType = type,
-                    UserId = userId,
+                    UserId = userId ?? Guid.Empty,
                     StartIndex = startIndex,
                     Limit = limit,
                     IsFavorite = isFavorite,
@@ -180,9 +180,9 @@ namespace Jellyfin.Api.Controllers
                 dtoOptions,
                 CancellationToken.None);
 
-            var user = userId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var fieldsList = dtoOptions.Fields.ToList();
             fieldsList.Remove(ItemFields.CanDelete);
@@ -210,9 +210,11 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Channels/{channelId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public ActionResult<BaseItemDto> GetChannel([FromRoute] Guid channelId, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetChannel([FromRoute] Guid channelId, [FromQuery] Guid? userId)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var item = channelId.Equals(Guid.Empty)
                 ? _libraryManager.GetUserRootFolder()
                 : _libraryManager.GetItemById(channelId);
@@ -250,17 +252,17 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
-            [FromQuery] string channelId,
-            [FromQuery] Guid userId,
+            [FromQuery] string? channelId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] RecordingStatus? status,
             [FromQuery] bool? isInProgress,
-            [FromQuery] string seriesTimerId,
+            [FromQuery] string? seriesTimerId,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string fields,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSeries,
@@ -279,7 +281,7 @@ namespace Jellyfin.Api.Controllers
                 new RecordingQuery
             {
                 ChannelId = channelId,
-                UserId = userId,
+                UserId = userId ?? Guid.Empty,
                 StartIndex = startIndex,
                 Limit = limit,
                 Status = status,
@@ -336,18 +338,18 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
-            [FromQuery] string channelId,
-            [FromQuery] Guid userId,
-            [FromQuery] string groupId,
+            [FromQuery] string? channelId,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? groupId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] RecordingStatus? status,
             [FromQuery] bool? isInProgress,
-            [FromQuery] string seriesTimerId,
+            [FromQuery] string? seriesTimerId,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string fields,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -365,7 +367,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Obsolete("This endpoint is obsolete.")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
-        public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid userId)
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
         {
             return new QueryResult<BaseItemDto>();
         }
@@ -379,9 +381,11 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Recordings/Folders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid userId)
+        public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
         {
-            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var folders = _liveTvManager.GetRecordingFolders(user);
 
             var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
@@ -403,9 +407,11 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Recordings/{recordingId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public ActionResult<BaseItemDto> GetRecording([FromRoute] Guid recordingId, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetRecording([FromRoute] Guid recordingId, [FromQuery] Guid? userId)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
 
             var dtoOptions = new DtoOptions()
@@ -457,7 +463,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Timers/Defaults")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string programId)
+        public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
         {
             return string.IsNullOrEmpty(programId)
                 ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
@@ -478,8 +484,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
-            [FromQuery] string channelId,
-            [FromQuery] string seriesTimerId,
+            [FromQuery] string? channelId,
+            [FromQuery] string? seriesTimerId,
             [FromQuery] bool? isActive,
             [FromQuery] bool? isScheduled)
         {
@@ -532,8 +538,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms(
-            [FromQuery] string channelIds,
-            [FromQuery] Guid userId,
+            [FromQuery] string? channelIds,
+            [FromQuery] Guid? userId,
             [FromQuery] DateTime? minStartDate,
             [FromQuery] bool? hasAired,
             [FromQuery] bool? isAiring,
@@ -547,20 +553,22 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isSports,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string sortBy,
-            [FromQuery] string sortOrder,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
+            [FromQuery] string? sortBy,
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
+            [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData,
-            [FromQuery] string seriesTimerId,
-            [FromQuery] Guid librarySeriesId,
-            [FromQuery] string fields,
+            [FromQuery] string? seriesTimerId,
+            [FromQuery] Guid? librarySeriesId,
+            [FromQuery] string? fields,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var query = new InternalItemsQuery(user)
             {
@@ -590,7 +598,7 @@ namespace Jellyfin.Api.Controllers
             {
                 query.IsSeries = true;
 
-                if (_libraryManager.GetItemById(librarySeriesId) is Series series)
+                if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
                 {
                     query.Name = series.Name;
                 }
@@ -684,7 +692,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetRecommendedPrograms(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] bool? isAiring,
             [FromQuery] bool? hasAired,
@@ -695,13 +703,15 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isSports,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string genreIds,
-            [FromQuery] string fields,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var query = new InternalItemsQuery(user)
             {
@@ -736,11 +746,11 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<BaseItemDto>> GetProgram(
             [FromRoute] string programId,
-            [FromQuery] Guid userId)
+            [FromQuery] Guid? userId)
         {
-            var user = userId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
         }
@@ -856,12 +866,12 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("SeriesTimers")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string sortBy, [FromQuery] SortOrder sortOrder)
+        public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
         {
             return await _liveTvManager.GetSeriesTimers(
                 new SeriesTimerQuery
                 {
-                    SortOrder = sortOrder,
+                    SortOrder = sortOrder ?? SortOrder.Ascending,
                     SortBy = sortBy
                 }, CancellationToken.None).ConfigureAwait(false);
         }
@@ -925,7 +935,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Obsolete("This endpoint is obsolete.")]
-        public ActionResult<BaseItemDto> GetRecordingGroup([FromQuery] Guid groupId)
+        public ActionResult<BaseItemDto> GetRecordingGroup([FromQuery] Guid? groupId)
         {
             return NotFound();
         }
@@ -966,7 +976,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("TunerHosts")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult DeleteTunerHost([FromQuery] string id)
+        public ActionResult DeleteTunerHost([FromQuery] string? id)
         {
             var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
             config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
@@ -990,10 +1000,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Adds a listings provider.
         /// </summary>
-        /// <param name="validateLogin">Validate login.</param>
-        /// <param name="validateListings">Validate listings.</param>
         /// <param name="pw">Password.</param>
         /// <param name="listingsProviderInfo">New listings info.</param>
+        /// <param name="validateListings">Validate listings.</param>
+        /// <param name="validateLogin">Validate login.</param>
         /// <response code="200">Created listings provider returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
         [HttpGet("ListingProviders")]
@@ -1001,10 +1011,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
-            [FromQuery] bool validateLogin,
-            [FromQuery] bool validateListings,
-            [FromQuery] string pw,
-            [FromBody] ListingsProviderInfo listingsProviderInfo)
+            [FromQuery] string? pw,
+            [FromBody] ListingsProviderInfo listingsProviderInfo,
+            [FromQuery] bool validateListings = false,
+            [FromQuery] bool validateLogin = false)
         {
             using var sha = SHA1.Create();
             if (!string.IsNullOrEmpty(pw))
@@ -1024,7 +1034,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("ListingProviders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult DeleteListingProvider([FromQuery] string id)
+        public ActionResult DeleteListingProvider([FromQuery] string? id)
         {
             _liveTvManager.DeleteListingsProvider(id);
             return NoContent();
@@ -1043,10 +1053,10 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
-            [FromQuery] string id,
-            [FromQuery] string type,
-            [FromQuery] string location,
-            [FromQuery] string country)
+            [FromQuery] string? id,
+            [FromQuery] string? type,
+            [FromQuery] string? location,
+            [FromQuery] string? country)
         {
             return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
         }
@@ -1079,7 +1089,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("ChannelMappingOptions")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string providerId)
+        public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
         {
             var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
 
@@ -1120,9 +1130,9 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
-            [FromQuery] string providerId,
-            [FromQuery] string tunerChannelId,
-            [FromQuery] string providerChannelId)
+            [FromQuery] string? providerId,
+            [FromQuery] string? tunerChannelId,
+            [FromQuery] string? providerChannelId)
         {
             return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
         }
@@ -1149,7 +1159,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Tuners/Discvover")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly)
+        public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
         {
             return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
         }
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index daf4bf419b..da400f5106 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
         [HttpGet("/Items/{itemId}/PlaybackInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
         {
             return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
         }
@@ -118,16 +118,16 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
             [FromRoute] Guid itemId,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] long? maxStreamingBitrate,
             [FromQuery] long? startTimeTicks,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? maxAudioChannels,
-            [FromQuery] string mediaSourceId,
-            [FromQuery] string liveStreamId,
-            [FromQuery] DeviceProfile deviceProfile,
-            [FromQuery] bool autoOpenLiveStream,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] DeviceProfile? deviceProfile,
+            [FromQuery] bool autoOpenLiveStream = false,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectStream = true,
             [FromQuery] bool enableTranscoding = true,
@@ -165,12 +165,12 @@ namespace Jellyfin.Api.Controllers
                         authInfo,
                         maxStreamingBitrate ?? profile.MaxStreamingBitrate,
                         startTimeTicks ?? 0,
-                        mediaSourceId,
+                        mediaSourceId ?? string.Empty,
                         audioStreamIndex,
                         subtitleStreamIndex,
                         maxAudioChannels,
                         info!.PlaySessionId!,
-                        userId,
+                        userId ?? Guid.Empty,
                         enableDirectPlay,
                         enableDirectStream,
                         enableTranscoding,
@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
                         PlaySessionId = info.PlaySessionId,
                         StartTimeTicks = startTimeTicks,
                         SubtitleStreamIndex = subtitleStreamIndex,
-                        UserId = userId,
+                        UserId = userId ?? Guid.Empty,
                         OpenToken = mediaSource.OpenToken
                     }).ConfigureAwait(false);
 
@@ -239,16 +239,16 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("/LiveStreams/Open")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
-            [FromQuery] string openToken,
-            [FromQuery] Guid userId,
-            [FromQuery] string playSessionId,
+            [FromQuery] string? openToken,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? playSessionId,
             [FromQuery] long? maxStreamingBitrate,
             [FromQuery] long? startTimeTicks,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? maxAudioChannels,
-            [FromQuery] Guid itemId,
-            [FromQuery] DeviceProfile deviceProfile,
+            [FromQuery] Guid? itemId,
+            [FromQuery] DeviceProfile? deviceProfile,
             [FromQuery] MediaProtocol[] directPlayProtocols,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectStream = true)
@@ -256,14 +256,14 @@ namespace Jellyfin.Api.Controllers
             var request = new LiveStreamRequest
             {
                 OpenToken = openToken,
-                UserId = userId,
+                UserId = userId ?? Guid.Empty,
                 PlaySessionId = playSessionId,
                 MaxStreamingBitrate = maxStreamingBitrate,
                 StartTimeTicks = startTimeTicks,
                 AudioStreamIndex = audioStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 MaxAudioChannels = maxAudioChannels,
-                ItemId = itemId,
+                ItemId = itemId ?? Guid.Empty,
                 DeviceProfile = deviceProfile,
                 EnableDirectPlay = enableDirectPlay,
                 EnableDirectStream = enableDirectStream,
@@ -280,7 +280,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("/LiveStreams/Close")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CloseLiveStream([FromQuery] string liveStreamId)
+        public ActionResult CloseLiveStream([FromQuery] string? liveStreamId)
         {
             _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
             return NoContent();
@@ -325,11 +325,13 @@ namespace Jellyfin.Api.Controllers
 
         private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
             Guid id,
-            Guid userId,
+            Guid? userId,
             string? mediaSourceId = null,
             string? liveStreamId = null)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var item = _libraryManager.GetItemById(id);
             var result = new PlaybackInfoResponse();
 
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 4dd3613c6b..144a7b554f 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -55,32 +55,22 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="enableImages">(Unused) Optional. include image information in output.</param>
-        /// <param name="enableUserData">(Unused) Optional. include user data.</param>
-        /// <param name="imageTypeLimit">(Unused) Optional. the max number of images to return, per image type.</param>
-        /// <param name="enableImageTypes">(Unused) Optional. The image types to include in the output.</param>
         /// <param name="fields">Optional. The fields to return.</param>
         /// <param name="categoryLimit">The max number of categories to return.</param>
         /// <param name="itemLimit">The max number of items to return per category.</param>
         /// <response code="200">Movie recommendations returned.</response>
         /// <returns>The list of movie recommendations.</returns>
         [HttpGet("Recommendations")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
         public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
-            [FromQuery] Guid userId,
-            [FromQuery] string parentId,
-            [FromQuery] bool? enableImages,
-            [FromQuery] bool? enableUserData,
-            [FromQuery] int? imageTypeLimit,
-            [FromQuery] string? enableImageTypes,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? parentId,
             [FromQuery] string? fields,
             [FromQuery] int categoryLimit = 5,
             [FromQuery] int itemLimit = 8)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
             var dtoOptions = new DtoOptions()
                 .AddItemFields(fields)
                 .AddClientFields(Request);
@@ -185,7 +175,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         private IEnumerable<RecommendationDto> GetWithDirector(
-            User user,
+            User? user,
             IEnumerable<string> names,
             int itemLimit,
             DtoOptions dtoOptions,
@@ -230,7 +220,7 @@ namespace Jellyfin.Api.Controllers
             }
         }
 
-        private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+        private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
         {
             var itemTypes = new List<string> { nameof(Movie) };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
@@ -270,7 +260,7 @@ namespace Jellyfin.Api.Controllers
             }
         }
 
-        private IEnumerable<RecommendationDto> GetSimilarTo(User user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+        private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
         {
             var itemTypes = new List<string> { nameof(Movie) };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 9ac74f199e..0d319137a4 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -83,31 +83,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -119,9 +119,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -258,7 +258,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns>
         [HttpGet("{genreName}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<BaseItemDto> GetMusicGenre([FromRoute] string genreName, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetMusicGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
 
@@ -273,9 +273,9 @@ namespace Jellyfin.Api.Controllers
                 item = _libraryManager.GetMusicGenre(genreName);
             }
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
 
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 03478a20a4..23cc23ce70 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -80,31 +80,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -259,7 +259,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid? userId)
         {
             var dtoOptions = new DtoOptions()
                 .AddClientFields(Request);
@@ -270,9 +270,9 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
 
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index d62404fc93..cf46604948 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -86,9 +86,9 @@ namespace Jellyfin.Api.Controllers
         public ActionResult AddToPlaylist(
             [FromRoute] string? playlistId,
             [FromQuery] string? ids,
-            [FromQuery] Guid userId)
+            [FromQuery] Guid? userId)
         {
-            _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId);
+            _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty);
             return NoContent();
         }
 
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 05a6edf4ed..9fcf041996 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -188,12 +188,12 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User id.</param>
         /// <param name="itemId">Item id.</param>
         /// <param name="mediaSourceId">The id of the MediaSource.</param>
-        /// <param name="canSeek">Indicates if the client can seek.</param>
         /// <param name="audioStreamIndex">The audio stream index.</param>
         /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
         /// <param name="playMethod">The play method.</param>
         /// <param name="liveStreamId">The live stream id.</param>
         /// <param name="playSessionId">The play session id.</param>
+        /// <param name="canSeek">Indicates if the client can seek.</param>
         /// <response code="204">Play start recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Users/{userId}/PlayingItems/{itemId}")]
@@ -202,13 +202,13 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> OnPlaybackStart(
             [FromRoute] Guid userId,
             [FromRoute] Guid itemId,
-            [FromQuery] string mediaSourceId,
-            [FromQuery] bool canSeek,
+            [FromQuery] string? mediaSourceId,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] PlayMethod playMethod,
-            [FromQuery] string liveStreamId,
-            [FromQuery] string playSessionId)
+            [FromQuery] string? liveStreamId,
+            [FromQuery] string playSessionId,
+            [FromQuery] bool canSeek = false)
         {
             var playbackStartInfo = new PlaybackStartInfo
             {
@@ -235,8 +235,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <param name="mediaSourceId">The id of the MediaSource.</param>
         /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
-        /// <param name="isPaused">Indicates if the player is paused.</param>
-        /// <param name="isMuted">Indicates if the player is muted.</param>
         /// <param name="audioStreamIndex">The audio stream index.</param>
         /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
         /// <param name="volumeLevel">Scale of 0-100.</param>
@@ -244,6 +242,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="liveStreamId">The live stream id.</param>
         /// <param name="playSessionId">The play session id.</param>
         /// <param name="repeatMode">The repeat mode.</param>
+        /// <param name="isPaused">Indicates if the player is paused.</param>
+        /// <param name="isMuted">Indicates if the player is muted.</param>
         /// <response code="204">Play progress recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")]
@@ -252,17 +252,17 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> OnPlaybackProgress(
             [FromRoute] Guid userId,
             [FromRoute] Guid itemId,
-            [FromQuery] string mediaSourceId,
+            [FromQuery] string? mediaSourceId,
             [FromQuery] long? positionTicks,
-            [FromQuery] bool isPaused,
-            [FromQuery] bool isMuted,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? volumeLevel,
             [FromQuery] PlayMethod playMethod,
-            [FromQuery] string liveStreamId,
+            [FromQuery] string? liveStreamId,
             [FromQuery] string playSessionId,
-            [FromQuery] RepeatMode repeatMode)
+            [FromQuery] RepeatMode repeatMode,
+            [FromQuery] bool isPaused = false,
+            [FromQuery] bool isMuted = false)
         {
             var playbackProgressInfo = new PlaybackProgressInfo
             {
@@ -304,11 +304,11 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> OnPlaybackStopped(
             [FromRoute] Guid userId,
             [FromRoute] Guid itemId,
-            [FromQuery] string mediaSourceId,
-            [FromQuery] string nextMediaType,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? nextMediaType,
             [FromQuery] long? positionTicks,
-            [FromQuery] string liveStreamId,
-            [FromQuery] string playSessionId)
+            [FromQuery] string? liveStreamId,
+            [FromQuery] string? playSessionId)
         {
             var playbackStopInfo = new PlaybackStopInfo
             {
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 6fff301297..1b26163cfc 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string providerName,
-            [FromQuery] bool includeAllLanguages)
+            [FromQuery] bool includeAllLanguages = false)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 14dc0815c7..2cbd32d2f7 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<SearchHintResult> Get(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery, Required] string? searchTerm,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? excludeItemTypes,
@@ -107,7 +107,7 @@ namespace Jellyfin.Api.Controllers
                 IncludePeople = includePeople,
                 IncludeStudios = includeStudios,
                 StartIndex = startIndex,
-                UserId = userId,
+                UserId = userId ?? Guid.Empty,
                 IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
                 ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
                 MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index bd738aa387..0c98a8e711 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<SessionInfo>> GetSessions(
-            [FromQuery] Guid controllableByUserId,
+            [FromQuery] Guid? controllableByUserId,
             [FromQuery] string? deviceId,
             [FromQuery] int? activeWithinSeconds)
         {
@@ -72,15 +72,15 @@ namespace Jellyfin.Api.Controllers
                 result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
             }
 
-            if (!controllableByUserId.Equals(Guid.Empty))
+            if (controllableByUserId.HasValue && !controllableByUserId.Equals(Guid.Empty))
             {
                 result = result.Where(i => i.SupportsRemoteControl);
 
-                var user = _userManager.GetUserById(controllableByUserId);
+                var user = _userManager.GetUserById(controllableByUserId.Value);
 
                 if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
                 {
-                    result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId));
+                    result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId.Value));
                 }
 
                 if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
@@ -371,8 +371,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? id,
             [FromQuery] string? playableMediaTypes,
             [FromQuery] string? supportedCommands,
-            [FromQuery] bool supportsMediaControl,
-            [FromQuery] bool supportsSync,
+            [FromQuery] bool supportsMediaControl = false,
+            [FromQuery] bool supportsSync = false,
             [FromQuery] bool supportsPersistentIdentifier = true)
         {
             if (string.IsNullOrWhiteSpace(id))
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 76cf2febfe..6f2787d933 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -82,31 +82,31 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] double? minCommunityRating,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string searchTerm,
-            [FromQuery] string parentId,
-            [FromQuery] string fields,
-            [FromQuery] string excludeItemTypes,
-            [FromQuery] string includeItemTypes,
-            [FromQuery] string filters,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? parentId,
+            [FromQuery] string? fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery] string? filters,
             [FromQuery] bool? isFavorite,
-            [FromQuery] string mediaTypes,
-            [FromQuery] string genres,
-            [FromQuery] string genreIds,
-            [FromQuery] string officialRatings,
-            [FromQuery] string tags,
-            [FromQuery] string years,
+            [FromQuery] string? mediaTypes,
+            [FromQuery] string? genres,
+            [FromQuery] string? genreIds,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] string enableImageTypes,
-            [FromQuery] string person,
-            [FromQuery] string personIds,
-            [FromQuery] string personTypes,
-            [FromQuery] string studios,
-            [FromQuery] string studioIds,
-            [FromQuery] Guid userId,
-            [FromQuery] string nameStartsWithOrGreater,
-            [FromQuery] string nameStartsWith,
-            [FromQuery] string nameLessThan,
+            [FromQuery] string? enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? studioIds,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -118,9 +118,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -259,14 +259,14 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the studio.</returns>
         [HttpGet("{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid? userId)
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
 
             var item = _libraryManager.GetStudio(name);
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
 
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index baedafaa63..1c38b8de5c 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -190,8 +190,8 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] int index,
             [FromRoute, Required] string? format,
             [FromQuery] long? endPositionTicks,
-            [FromQuery] bool copyTimestamps,
-            [FromQuery] bool addVttTimeMap,
+            [FromQuery] bool copyTimestamps = false,
+            [FromQuery] bool addVttTimeMap = false,
             [FromRoute] long startPositionTicks = 0)
         {
             if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index e1a99a1385..bf3c1e2b14 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -44,9 +44,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id.</param>
         /// <param name="mediaType">The media types.</param>
         /// <param name="type">The type.</param>
-        /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
         /// <param name="startIndex">Optional. The start index.</param>
         /// <param name="limit">Optional. The limit.</param>
+        /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
         /// <response code="200">Suggestions returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
         [HttpGet("/Users/{userId}/Suggestions")]
@@ -55,9 +55,9 @@ namespace Jellyfin.Api.Controllers
             [FromRoute] Guid userId,
             [FromQuery] string? mediaType,
             [FromQuery] string? type,
-            [FromQuery] bool enableTotalRecordCount,
             [FromQuery] int? startIndex,
-            [FromQuery] int? limit)
+            [FromQuery] int? limit,
+            [FromQuery] bool enableTotalRecordCount = false)
         {
             var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
 
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index bd65abd509..645495551b 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("/Trailers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
             [FromQuery] bool? hasThemeVideo,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 80b6a24883..e5b0436214 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("NextUp")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -93,12 +93,14 @@ namespace Jellyfin.Api.Controllers
                     ParentId = parentId,
                     SeriesId = seriesId,
                     StartIndex = startIndex,
-                    UserId = userId,
+                    UserId = userId ?? Guid.Empty,
                     EnableTotalRecordCount = enableTotalRecordCount
                 },
                 options);
 
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
 
@@ -125,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Upcoming")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -135,7 +137,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
 
@@ -191,7 +195,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
             [FromRoute] string? seriesId,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] int? season,
             [FromQuery] string? seasonId,
@@ -206,7 +210,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableUserData,
             [FromQuery] string? sortBy)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             List<BaseItem> episodes;
 
@@ -312,7 +318,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
             [FromRoute] string? seriesId,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
@@ -322,7 +328,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? enableImageTypes,
             [FromQuery] bool? enableUserData)
         {
-            var user = _userManager.GetUserById(userId);
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             if (!(_libraryManager.GetItemById(seriesId) is Series series))
             {
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index ca804ebc95..cedda3b9d2 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -180,7 +180,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
         [HttpPost("/Users/{userId}/Items/{itemId}/Rating")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool likes)
+        public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool? likes)
         {
             return UpdateUserItemRatingInternal(userId, itemId, likes);
         }
@@ -264,7 +264,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
             [FromRoute] Guid userId,
-            [FromQuery] Guid parentId,
+            [FromQuery] Guid? parentId,
             [FromQuery] string? fields,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isPlayed,
@@ -297,7 +297,7 @@ namespace Jellyfin.Api.Controllers
                     IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
                     IsPlayed = isPlayed,
                     Limit = limit,
-                    ParentId = parentId,
+                    ParentId = parentId ?? Guid.Empty,
                     UserId = userId,
                 }, dtoOptions);
 
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index ad8927262b..f4bd451efe 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -56,8 +56,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="userId">User id.</param>
         /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
-        /// <param name="includeHidden">Whether or not to include hidden content.</param>
         /// <param name="presetViews">Preset views.</param>
+        /// <param name="includeHidden">Whether or not to include hidden content.</param>
         /// <response code="200">User views returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
         [HttpGet("/Users/{userId}/Views")]
@@ -65,8 +65,8 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
             [FromRoute] Guid userId,
             [FromQuery] bool? includeExternalContent,
-            [FromQuery] bool includeHidden,
-            [FromQuery] string? presetViews)
+            [FromQuery] string? presetViews,
+            [FromQuery] bool includeHidden = false)
         {
             var query = new UserViewQuery
             {
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index fb1141984d..e2a44427b8 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -53,9 +53,11 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{itemId}/AdditionalParts")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId)
+        public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid? userId)
         {
-            var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null;
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
 
             var item = itemId.Equals(Guid.Empty)
                 ? (!userId.Equals(Guid.Empty)
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index a66a3951e1..d09b016a9a 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] string? enableImageTypes,
-            [FromQuery] Guid userId,
+            [FromQuery] Guid? userId,
             [FromQuery] bool recursive = true,
             [FromQuery] bool? enableImages = true)
         {
@@ -86,9 +86,9 @@ namespace Jellyfin.Api.Controllers
             User? user = null;
             BaseItem parentItem;
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                user = _userManager.GetUserById(userId);
+                user = _userManager.GetUserById(userId.Value);
                 parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
             }
             else
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{year}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid userId)
+        public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid? userId)
         {
             var item = _libraryManager.GetYear(year);
             if (item == null)
@@ -187,9 +187,9 @@ namespace Jellyfin.Api.Controllers
             var dtoOptions = new DtoOptions()
                 .AddClientFields(Request);
 
-            if (!userId.Equals(Guid.Empty))
+            if (userId.HasValue && !userId.Equals(Guid.Empty))
             {
-                var user = _userManager.GetUserById(userId);
+                var user = _userManager.GetUserById(userId.Value);
                 return _dtoService.GetBaseItemDto(item, dtoOptions, user);
             }
 
diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
index fd0c315048..b922e76cfd 100644
--- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
+++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs
@@ -4,7 +4,6 @@ using System.Linq;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -21,14 +20,16 @@ namespace Jellyfin.Api.Helpers
             IUserManager userManager,
             ILibraryManager libraryManager,
             IDtoService dtoService,
-            Guid userId,
+            Guid? userId,
             string id,
             string? excludeArtistIds,
             int? limit,
             Type[] includeTypes,
             Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
         {
-            var user = !userId.Equals(Guid.Empty) ? userManager.GetUserById(userId) : null;
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? userManager.GetUserById(userId.Value)
+                : null;
 
             var item = string.IsNullOrEmpty(id) ?
                 (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
@@ -38,11 +39,10 @@ namespace Jellyfin.Api.Helpers
             {
                 IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
                 Recursive = true,
-                DtoOptions = dtoOptions
+                DtoOptions = dtoOptions,
+                ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds)
             };
 
-            query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
-
             var inputItems = libraryManager.GetItemList(query);
 
             var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)

From 2328ec59c92bbf3e2846f9b0f307f10ad0d958c6 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 11 Jul 2020 11:14:23 +0200
Subject: [PATCH 299/463] Migrate AudioService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AudioController.cs  | 183 +++++++++++++++++
 Jellyfin.Api/Helpers/StreamingHelpers.cs     | 194 +++++++++++++++++++
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs |  55 ++++++
 Jellyfin.Api/Models/StreamState.cs           | 145 ++++++++++++++
 4 files changed, 577 insertions(+)
 create mode 100644 Jellyfin.Api/Controllers/AudioController.cs
 create mode 100644 Jellyfin.Api/Helpers/StreamingHelpers.cs
 create mode 100644 Jellyfin.Api/Models/StreamState.cs

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
new file mode 100644
index 0000000000..39df1e1b13
--- /dev/null
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Controllers
+{
+
+    /// <summary>
+    /// The audio controller.
+    /// </summary>
+    public class AudioController : BaseJellyfinApiController
+    {
+        private readonly IDlnaManager _dlnaManager;
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioController"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{AuidoController}"/> interface.</param>
+        public AudioController(IDlnaManager dlnaManager, ILogger<AudioController> logger)
+        {
+            _dlnaManager = dlnaManager;
+            _logger = logger;
+        }
+
+        [HttpGet("{id}/stream.{container}")]
+        [HttpGet("{id}/stream")]
+        [HttpHead("{id}/stream.{container}")]
+        [HttpGet("{id}/stream")]
+        public async Task<ActionResult> GetAudioStream(
+            [FromRoute] string id,
+            [FromRoute] string container,
+            [FromQuery] bool Static,
+            [FromQuery] string tag)
+        {
+            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+            var cancellationTokenSource = new CancellationTokenSource();
+
+            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
+
+            if (Static && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+
+                using (state)
+                {
+                    var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+                    // TODO: Don't hardcode this
+                    outputHeaders[HeaderNames.ContentType] = MimeTypes.GetMimeType("file.ts");
+
+                    return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, _logger, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    };
+                }
+            }
+
+            // Static remote stream
+            if (Static && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+
+                using (state)
+                {
+                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+                }
+            }
+
+            if (Static && state.InputProtocol != MediaProtocol.File)
+            {
+                throw new ArgumentException(string.Format($"Input protocol {state.InputProtocol} cannot be streamed statically."));
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = File.Exists(outputPath);
+
+            var transcodingJob = TranscodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, Static || isTranscodeCached, Request, _dlnaManager);
+
+            // Static stream
+            if (Static)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                using (state)
+                {
+                    if (state.MediaSource.IsInfiniteStream)
+                    {
+                        var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+                        {
+                            [HeaderNames.ContentType] = contentType
+                        };
+
+
+                        return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, _logger, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        };
+                    }
+
+                    TimeSpan? cacheDuration = null;
+
+                    if (!string.IsNullOrEmpty(tag))
+                    {
+                        cacheDuration = TimeSpan.FromDays(365);
+                    }
+
+                    return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+                    {
+                        ResponseHeaders = responseHeaders,
+                        ContentType = contentType,
+                        IsHeadRequest = isHeadRequest,
+                        Path = state.MediaPath,
+                        CacheDuration = cacheDuration
+
+                    }).ConfigureAwait(false);
+                }
+            }
+
+            //// Not static but transcode cache file exists
+            //if (isTranscodeCached && state.VideoRequest == null)
+            //{
+            //    var contentType = state.GetMimeType(outputPath);
+
+            //    try
+            //    {
+            //        if (transcodingJob != null)
+            //        {
+            //            ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
+            //        }
+
+            //        return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            //        {
+            //            ResponseHeaders = responseHeaders,
+            //            ContentType = contentType,
+            //            IsHeadRequest = isHeadRequest,
+            //            Path = outputPath,
+            //            FileShare = FileShare.ReadWrite,
+            //            OnComplete = () =>
+            //            {
+            //                if (transcodingJob != null)
+            //                {
+            //                    ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
+            //                }
+            //            }
+
+            //        }).ConfigureAwait(false);
+            //    }
+            //    finally
+            //    {
+            //        state.Dispose();
+            //    }
+            //}
+
+            // Need to start ffmpeg
+            try
+            {
+                return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+            }
+            catch
+            {
+                state.Dispose();
+
+                throw;
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
new file mode 100644
index 0000000000..4cebf40f6d
--- /dev/null
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Api.Models;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// The streaming helpers
+    /// </summary>
+    public class StreamingHelpers
+    {
+        /// <summary>
+        /// Adds the dlna headers.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="responseHeaders">The response headers.</param>
+        /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
+        /// <param name="request">The <see cref="HttpRequest"/>.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        public static void AddDlnaHeaders(
+            StreamState state,
+            IHeaderDictionary responseHeaders,
+            bool isStaticallyStreamed,
+            HttpRequest request,
+            IDlnaManager dlnaManager)
+        {
+            if (!state.EnableDlnaHeaders)
+            {
+                return;
+            }
+
+            var profile = state.DeviceProfile;
+
+            StringValues transferMode = request.Headers["transferMode.dlna.org"];
+            responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
+            responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
+
+            if (state.RunTimeTicks.HasValue)
+            {
+                if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
+                {
+                    var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
+                    responseHeaders.Add("MediaInfo.sec", string.Format(
+                        CultureInfo.InvariantCulture,
+                        "SEC_Duration={0};",
+                        Convert.ToInt32(ms)));
+                }
+
+                if (!isStaticallyStreamed && profile != null)
+                {
+                    AddTimeSeekResponseHeaders(state, responseHeaders);
+                }
+            }
+
+            if (profile == null)
+            {
+                profile = dlnaManager.GetDefaultProfile();
+            }
+
+            var audioCodec = state.ActualOutputAudioCodec;
+
+            if (state.VideoRequest == null)
+            {
+                responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
+                    state.OutputContainer,
+                    audioCodec,
+                    state.OutputAudioBitrate,
+                    state.OutputAudioSampleRate,
+                    state.OutputAudioChannels,
+                    state.OutputAudioBitDepth,
+                    isStaticallyStreamed,
+                    state.RunTimeTicks,
+                    state.TranscodeSeekInfo));
+            }
+            else
+            {
+                var videoCodec = state.ActualOutputVideoCodec;
+
+                responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildVideoHeader(
+                    state.OutputContainer,
+                    videoCodec,
+                    audioCodec,
+                    state.OutputWidth,
+                    state.OutputHeight,
+                    state.TargetVideoBitDepth,
+                    state.OutputVideoBitrate,
+                    state.TargetTimestamp,
+                    isStaticallyStreamed,
+                    state.RunTimeTicks,
+                    state.TargetVideoProfile,
+                    state.TargetVideoLevel,
+                    state.TargetFramerate,
+                    state.TargetPacketLength,
+                    state.TranscodeSeekInfo,
+                    state.IsTargetAnamorphic,
+                    state.IsTargetInterlaced,
+                    state.TargetRefFrames,
+                    state.TargetVideoStreamCount,
+                    state.TargetAudioStreamCount,
+                    state.TargetVideoCodecTag,
+                    state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Parses the dlna headers.
+        /// </summary>
+        /// <param name="startTimeTicks">The start time ticks.</param>
+        /// <param name="request">The <see cref="HttpRequest"/>.</param>
+        public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request)
+        {
+            if (!startTimeTicks.HasValue)
+            {
+                var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
+
+                startTimeTicks = ParseTimeSeekHeader(timeSeek);
+            }
+        }
+
+        /// <summary>
+        /// Parses the time seek header.
+        /// </summary>
+        public long? ParseTimeSeekHeader(string value)
+        {
+            if (string.IsNullOrWhiteSpace(value))
+            {
+                return null;
+            }
+
+            const string Npt = "npt=";
+            if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
+            {
+                throw new ArgumentException("Invalid timeseek header");
+            }
+            int index = value.IndexOf('-');
+            value = index == -1
+                ? value.Substring(Npt.Length)
+                : value.Substring(Npt.Length, index - Npt.Length);
+
+            if (value.IndexOf(':') == -1)
+            {
+                // Parses npt times in the format of '417.33'
+                if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
+                {
+                    return TimeSpan.FromSeconds(seconds).Ticks;
+                }
+
+                throw new ArgumentException("Invalid timeseek header");
+            }
+
+            // Parses npt times in the format of '10:19:25.7'
+            var tokens = value.Split(new[] { ':' }, 3);
+            double secondsSum = 0;
+            var timeFactor = 3600;
+
+            foreach (var time in tokens)
+            {
+                if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
+                {
+                    secondsSum += digit * timeFactor;
+                }
+                else
+                {
+                    throw new ArgumentException("Invalid timeseek header");
+                }
+                timeFactor /= 60;
+            }
+            return TimeSpan.FromSeconds(secondsSum).Ticks;
+        }
+
+        public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders)
+        {
+            var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+            var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+
+            responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
+                CultureInfo.InvariantCulture,
+                "npt={0}-{1}/{1}",
+                startSeconds,
+                runtimeSeconds));
+            responseHeaders.Add("X-AvailableSeekRange", string.Format(
+                CultureInfo.InvariantCulture,
+                "1 npt={0}-{1}",
+                startSeconds,
+                runtimeSeconds));
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 44f662e6e0..7db75387a1 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -5,10 +5,12 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models;
 using Jellyfin.Api.Models.PlaybackDtos;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Helpers
@@ -61,6 +63,14 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        public static TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+            }
+        }
+
         /// <summary>
         /// Ping transcoding job.
         /// </summary>
@@ -350,5 +360,50 @@ namespace Jellyfin.Api.Helpers
                 throw new AggregateException("Error deleting HLS files", exs);
             }
         }
+
+        public void ReportTranscodingProgress(
+        TranscodingJob job,
+        StreamState state,
+        TimeSpan? transcodingPosition,
+        float? framerate,
+        double? percentComplete,
+        long? bytesTranscoded,
+        int? bitRate)
+        {
+            var ticks = transcodingPosition?.Ticks;
+
+            if (job != null)
+            {
+                job.Framerate = framerate;
+                job.CompletionPercentage = percentComplete;
+                job.TranscodingPositionTicks = ticks;
+                job.BytesTranscoded = bytesTranscoded;
+                job.BitRate = bitRate;
+            }
+
+            var deviceId = state.Request.DeviceId;
+
+            if (!string.IsNullOrWhiteSpace(deviceId))
+            {
+                var audioCodec = state.ActualOutputAudioCodec;
+                var videoCodec = state.ActualOutputVideoCodec;
+
+                _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
+                {
+                    Bitrate = bitRate ?? state.TotalOutputBitrate,
+                    AudioCodec = audioCodec,
+                    VideoCodec = videoCodec,
+                    Container = state.OutputContainer,
+                    Framerate = framerate,
+                    CompletionPercentage = percentComplete,
+                    Width = state.OutputWidth,
+                    Height = state.OutputHeight,
+                    AudioChannels = state.OutputAudioChannels,
+                    IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
+                    IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+                    TranscodeReasons = state.TranscodeReasons
+                });
+            }
+        }
     }
 }
diff --git a/Jellyfin.Api/Models/StreamState.cs b/Jellyfin.Api/Models/StreamState.cs
new file mode 100644
index 0000000000..9fe5f52c3e
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamState.cs
@@ -0,0 +1,145 @@
+using System;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+
+namespace Jellyfin.Api.Models
+{
+    public class StreamState : EncodingJobInfo, IDisposable
+    {
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private bool _disposed = false;
+
+        public string RequestedUrl { get; set; }
+
+        public StreamRequest Request
+        {
+            get => (StreamRequest)BaseRequest;
+            set
+            {
+                BaseRequest = value;
+
+                IsVideoRequest = VideoRequest != null;
+            }
+        }
+
+        public TranscodingThrottler TranscodingThrottler { get; set; }
+
+        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
+
+        public IDirectStreamProvider DirectStreamProvider { get; set; }
+
+        public string WaitForPath { get; set; }
+
+        public bool IsOutputVideo => Request is VideoStreamRequest;
+
+        public int SegmentLength
+        {
+            get
+            {
+                if (Request.SegmentLength.HasValue)
+                {
+                    return Request.SegmentLength.Value;
+                }
+
+                if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
+                {
+                    var userAgent = UserAgent ?? string.Empty;
+
+                    if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
+                    {
+                        if (IsSegmentedLiveStream)
+                        {
+                            return 6;
+                        }
+
+                        return 6;
+                    }
+
+                    if (IsSegmentedLiveStream)
+                    {
+                        return 3;
+                    }
+
+                    return 6;
+                }
+
+                return 3;
+            }
+        }
+
+        public int MinSegments
+        {
+            get
+            {
+                if (Request.MinSegments.HasValue)
+                {
+                    return Request.MinSegments.Value;
+                }
+
+                return SegmentLength >= 10 ? 2 : 3;
+            }
+        }
+
+        public string UserAgent { get; set; }
+
+        public bool EstimateContentLength { get; set; }
+
+        public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+        public bool EnableDlnaHeaders { get; set; }
+
+        public DeviceProfile DeviceProfile { get; set; }
+
+        public TranscodingJobDto TranscodingJob { get; set; }
+
+        public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
+            : base(transcodingType)
+        {
+            _mediaSourceManager = mediaSourceManager;
+        }
+
+        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+        {
+            TranscodingJobHelper.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (disposing)
+            {
+                // REVIEW: Is this the right place for this?
+                if (MediaSource.RequiresClosing
+                    && string.IsNullOrWhiteSpace(Request.LiveStreamId)
+                    && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
+                {
+                    _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
+                }
+
+                TranscodingThrottler?.Dispose();
+            }
+
+            TranscodingThrottler = null;
+            TranscodingJob = null;
+
+            _disposed = true;
+        }
+    }
+}

From ee03b919f98032d2c49bd1613a5ca0874790062d Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 12 Jul 2020 20:11:59 +0200
Subject: [PATCH 300/463] Fix parsing

---
 .../Controllers/ConfigurationController.cs    |  3 +-
 Jellyfin.Api/Controllers/PluginsController.cs |  3 +-
 .../Json/Converters/JsonDoubleConverter.cs    | 56 +++++++++++++++++++
 MediaBrowser.Common/Json/JsonDefaults.cs      |  1 +
 .../BaseApplicationConfiguration.cs           |  8 ++-
 5 files changed, 68 insertions(+), 3 deletions(-)
 create mode 100644 MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 13933cb33b..d3c29969b7 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -2,6 +2,7 @@ using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.ConfigurationDtos;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
@@ -87,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
-            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
+            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, JsonDefaults.GetOptions()).ConfigureAwait(false);
             _configurationManager.SaveConfiguration(key, configuration);
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 056395a51d..9b5529c370 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.PluginDtos;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Model.Plugins;
@@ -118,7 +119,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType)
+            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, JsonDefaults.GetOptions())
                 .ConfigureAwait(false);
 
             plugin.UpdateConfiguration(configuration);
diff --git a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
new file mode 100644
index 0000000000..e5e9f28dae
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// Double to String JSON converter.
+    /// Web client send quoted doubles.
+    /// </summary>
+    public class JsonDoubleConverter : JsonConverter<double>
+    {
+        /// <summary>
+        /// Read JSON string as double.
+        /// </summary>
+        /// <param name="reader"><see cref="Utf8JsonReader"/>.</param>
+        /// <param name="typeToConvert">Type.</param>
+        /// <param name="options">Options.</param>
+        /// <returns>Parsed value.</returns>
+        public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType == JsonTokenType.String)
+            {
+                // try to parse number directly from bytes
+                var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+                if (Utf8Parser.TryParse(span, out double number, out var bytesConsumed) && span.Length == bytesConsumed)
+                {
+                    return number;
+                }
+
+                // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters
+                if (double.TryParse(reader.GetString(), out number))
+                {
+                    return number;
+                }
+            }
+
+            // fallback to default handling
+            return reader.GetDouble();
+        }
+
+        /// <summary>
+        /// Write double to JSON string.
+        /// </summary>
+        /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param>
+        /// <param name="value">Value to write.</param>
+        /// <param name="options">Options.</param>
+        public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
+        {
+            writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo));
+        }
+    }
+}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 13f2f060b2..36ab6d900a 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -32,6 +32,7 @@ namespace MediaBrowser.Common.Json
             options.Converters.Add(new JsonStringEnumConverter());
             options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
             options.Converters.Add(new JsonInt64Converter());
+            options.Converters.Add(new JsonDoubleConverter());
 
             return options;
         }
diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
index cdd322c948..db06c06fc1 100644
--- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -44,7 +44,13 @@ namespace MediaBrowser.Model.Configuration
         public string PreviousVersionStr
         {
             get => PreviousVersion?.ToString();
-            set => PreviousVersion = Version.Parse(value);
+            set
+            {
+                if (Version.TryParse(value, out var version))
+                {
+                    PreviousVersion = version;
+                }
+            }
         }
 
         /// <summary>

From 9f567e6471e2a70aaa3028fb6b183e24987f627b Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 14 Jul 2020 12:39:58 +0200
Subject: [PATCH 301/463] Don't recreate JsonSerializerOptions every time

---
 MediaBrowser.Common/Json/JsonDefaults.cs | 30 +++++++++++++++---------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 36ab6d900a..c8217f9ab0 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -9,6 +9,8 @@ namespace MediaBrowser.Common.Json
     /// </summary>
     public static class JsonDefaults
     {
+        private static JsonSerializerOptions _defaultOptions;
+
         /// <summary>
         /// Gets the default <see cref="JsonSerializerOptions" /> options.
         /// </summary>
@@ -21,20 +23,26 @@ namespace MediaBrowser.Common.Json
         /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
         public static JsonSerializerOptions GetOptions()
         {
-            var options = new JsonSerializerOptions
+            if (_defaultOptions == null)
             {
-                ReadCommentHandling = JsonCommentHandling.Disallow,
-                WriteIndented = false
-            };
+                var options = new JsonSerializerOptions
+                {
+                    ReadCommentHandling = JsonCommentHandling.Disallow,
+                    WriteIndented = false
+                };
 
-            options.Converters.Add(new JsonGuidConverter());
-            options.Converters.Add(new JsonInt32Converter());
-            options.Converters.Add(new JsonStringEnumConverter());
-            options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
-            options.Converters.Add(new JsonInt64Converter());
-            options.Converters.Add(new JsonDoubleConverter());
+                options.Converters.Add(new JsonGuidConverter());
+                options.Converters.Add(new JsonInt32Converter());
+                options.Converters.Add(new JsonStringEnumConverter());
+                options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
+                options.Converters.Add(new JsonInt64Converter());
+                options.Converters.Add(new JsonDoubleConverter());
 
-            return options;
+                _defaultOptions = options;
+                return _defaultOptions;
+            }
+
+            return _defaultOptions;
         }
 
         /// <summary>

From 262e19b691762eb4fb00c50737c5decd62f35d4a Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 14 Jul 2020 13:26:47 +0200
Subject: [PATCH 302/463] Add X-Response-Time-ms header and log slow server
 response time

---
 .../Middleware/ResponseTimeMiddleware.cs      | 78 +++++++++++++++++++
 Jellyfin.Server/Startup.cs                    |  2 +
 .../Configuration/ServerConfiguration.cs      | 13 ++++
 3 files changed, 93 insertions(+)
 create mode 100644 Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs

diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
new file mode 100644
index 0000000000..3122d92cbc
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -0,0 +1,78 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Middleware
+{
+    /// <summary>
+    /// Response time middleware.
+    /// </summary>
+    public class ResponseTimeMiddleware
+    {
+        private const string ResponseHeaderResponseTime = "X-Response-Time-ms";
+
+        private readonly RequestDelegate _next;
+        private readonly ILogger<ResponseTimeMiddleware> _logger;
+
+        private readonly bool _enableWarning;
+        private readonly long _warningThreshold;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
+        /// </summary>
+        /// <param name="next">Next request delegate.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public ResponseTimeMiddleware(
+            RequestDelegate next,
+            ILogger<ResponseTimeMiddleware> logger,
+            IServerConfigurationManager serverConfigurationManager)
+        {
+            _next = next;
+            _logger = logger;
+
+            _enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
+            _warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
+        }
+
+        /// <summary>
+        /// Invoke request.
+        /// </summary>
+        /// <param name="context">Request context.</param>
+        /// <returns>Task.</returns>
+        public async Task Invoke(HttpContext context)
+        {
+            var watch = new Stopwatch();
+            watch.Start();
+
+            context.Response.OnStarting(() =>
+            {
+                watch.Stop();
+                LogWarning(context, watch);
+                var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
+                context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
+                return Task.CompletedTask;
+            });
+
+            // Call the next delegate/middleware in the pipeline
+            await this._next(context).ConfigureAwait(false);
+        }
+
+        private void LogWarning(HttpContext context, Stopwatch watch)
+        {
+            if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
+            {
+                _logger.LogWarning(
+                    "Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}",
+                    context.Request.GetDisplayUrl(),
+                    context.Connection.RemoteIpAddress,
+                    watch.Elapsed,
+                    context.Response.StatusCode);
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index a7bc156148..edf023fa24 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -63,6 +63,8 @@ namespace Jellyfin.Server
 
             app.UseMiddleware<ExceptionMiddleware>();
 
+            app.UseMiddleware<ResponseTimeMiddleware>();
+
             app.UseWebSockets();
 
             app.UseResponseCompression();
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index afbe02dd36..56389d524f 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -252,6 +252,16 @@ namespace MediaBrowser.Model.Configuration
 
         public string[] UninstalledPlugins { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
+        /// </summary>
+        public bool EnableSlowResponseWarning { get; set; }
+
+        /// <summary>
+        /// Gets or sets the threshold for the slow response time warning in ms.
+        /// </summary>
+        public long SlowResponseThresholdMs { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -351,6 +361,9 @@ namespace MediaBrowser.Model.Configuration
                     DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
                 }
             };
+
+            EnableSlowResponseWarning = true;
+            SlowResponseThresholdMs = 500;
         }
     }
 

From c6a0306a34e72fca424545bd33772e91aab92ed7 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 14 Jul 2020 20:20:24 +0200
Subject: [PATCH 303/463] Move field to the controller

---
 .../Controllers/ConfigurationController.cs    |  4 ++-
 Jellyfin.Api/Controllers/PluginsController.cs |  5 ++--
 MediaBrowser.Common/Json/JsonDefaults.cs      | 30 +++++++------------
 3 files changed, 17 insertions(+), 22 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index d3c29969b7..7d262ed595 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -23,6 +23,8 @@ namespace Jellyfin.Api.Controllers
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IMediaEncoder _mediaEncoder;
 
+        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
         /// </summary>
@@ -88,7 +90,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
         {
             var configurationType = _configurationManager.GetConfigurationType(key);
-            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, JsonDefaults.GetOptions()).ConfigureAwait(false);
+            var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
             _configurationManager.SaveConfiguration(key, configuration);
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 9b5529c370..770d74838d 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -27,6 +26,8 @@ namespace Jellyfin.Api.Controllers
         private readonly IApplicationHost _appHost;
         private readonly IInstallationManager _installationManager;
 
+        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="PluginsController"/> class.
         /// </summary>
@@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, JsonDefaults.GetOptions())
+            var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
                 .ConfigureAwait(false);
 
             plugin.UpdateConfiguration(configuration);
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index c8217f9ab0..36ab6d900a 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -9,8 +9,6 @@ namespace MediaBrowser.Common.Json
     /// </summary>
     public static class JsonDefaults
     {
-        private static JsonSerializerOptions _defaultOptions;
-
         /// <summary>
         /// Gets the default <see cref="JsonSerializerOptions" /> options.
         /// </summary>
@@ -23,26 +21,20 @@ namespace MediaBrowser.Common.Json
         /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
         public static JsonSerializerOptions GetOptions()
         {
-            if (_defaultOptions == null)
+            var options = new JsonSerializerOptions
             {
-                var options = new JsonSerializerOptions
-                {
-                    ReadCommentHandling = JsonCommentHandling.Disallow,
-                    WriteIndented = false
-                };
-
-                options.Converters.Add(new JsonGuidConverter());
-                options.Converters.Add(new JsonInt32Converter());
-                options.Converters.Add(new JsonStringEnumConverter());
-                options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
-                options.Converters.Add(new JsonInt64Converter());
-                options.Converters.Add(new JsonDoubleConverter());
+                ReadCommentHandling = JsonCommentHandling.Disallow,
+                WriteIndented = false
+            };
 
-                _defaultOptions = options;
-                return _defaultOptions;
-            }
+            options.Converters.Add(new JsonGuidConverter());
+            options.Converters.Add(new JsonInt32Converter());
+            options.Converters.Add(new JsonStringEnumConverter());
+            options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
+            options.Converters.Add(new JsonInt64Converter());
+            options.Converters.Add(new JsonDoubleConverter());
 
-            return _defaultOptions;
+            return options;
         }
 
         /// <summary>

From 9a2bcd6266fb222491abe6ea31d5e7e734699d5f Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 15 Jul 2020 16:15:17 +0200
Subject: [PATCH 304/463] Move SyncPlay api to Jellyfin.Api

---
 .../Controllers/SyncPlayController.cs         | 186 +++++++++++
 .../Controllers/TimeSyncController.cs         |  39 +++
 MediaBrowser.Api/SyncPlay/SyncPlayService.cs  | 302 ------------------
 MediaBrowser.Api/SyncPlay/TimeSyncService.cs  |  52 ---
 4 files changed, 225 insertions(+), 354 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/SyncPlayController.cs
 create mode 100644 Jellyfin.Api/Controllers/TimeSyncController.cs
 delete mode 100644 MediaBrowser.Api/SyncPlay/SyncPlayService.cs
 delete mode 100644 MediaBrowser.Api/SyncPlay/TimeSyncService.cs

diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
new file mode 100644
index 0000000000..99f828518f
--- /dev/null
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Threading;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The sync play controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class SyncPlayController : BaseJellyfinApiController
+    {
+        private readonly ISessionManager _sessionManager;
+        private readonly IAuthorizationContext _authorizationContext;
+        private readonly ISyncPlayManager _syncPlayManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SyncPlayController"/> class.
+        /// </summary>
+        /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
+        public SyncPlayController(
+            ISessionManager sessionManager,
+            IAuthorizationContext authorizationContext,
+            ISyncPlayManager syncPlayManager)
+        {
+            _sessionManager = sessionManager;
+            _authorizationContext = authorizationContext;
+            _syncPlayManager = syncPlayManager;
+        }
+
+        /// <summary>
+        /// Create a new SyncPlay group.
+        /// </summary>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("New")]
+        public ActionResult CreateNewGroup()
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Join an existing SyncPlay group.
+        /// </summary>
+        /// <param name="groupId">The sync play group id.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("Join")]
+        public ActionResult JoinGroup([FromQuery, Required] Guid groupId)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+
+            var joinRequest = new JoinGroupRequest()
+            {
+                GroupId = groupId
+            };
+
+            _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Leave the joined SyncPlay group.
+        /// </summary>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost("Leave")]
+        public ActionResult LeaveGroup()
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets all SyncPlay groups.
+        /// </summary>
+        /// <param name="filterItemId">Optional. Filter by item id.</param>
+        /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
+        [HttpGet("List")]
+        public ActionResult<IEnumerable<GroupInfoView>> GetSyncPlayGroups([FromQuery] Guid? filterItemId)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
+        }
+
+        /// <summary>
+        /// Request play in SyncPlay group.
+        /// </summary>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost]
+        public ActionResult Play()
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            var syncPlayRequest = new PlaybackRequest()
+            {
+                Type = PlaybackRequestType.Play
+            };
+            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Request pause in SyncPlay group.
+        /// </summary>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost]
+        public ActionResult Pause()
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            var syncPlayRequest = new PlaybackRequest()
+            {
+                Type = PlaybackRequestType.Pause
+            };
+            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Request seek in SyncPlay group.
+        /// </summary>
+        /// <param name="positionTicks">The playback position in ticks.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost]
+        public ActionResult Seek([FromQuery] long positionTicks)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            var syncPlayRequest = new PlaybackRequest()
+            {
+                Type = PlaybackRequestType.Seek,
+                PositionTicks = positionTicks
+            };
+            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Request group wait in SyncPlay group while buffering.
+        /// </summary>
+        /// <param name="when">When the request has been made by the client.</param>
+        /// <param name="positionTicks">The playback position in ticks.</param>
+        /// <param name="bufferingDone">Whether the buffering is done.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost]
+        public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            var syncPlayRequest = new PlaybackRequest()
+            {
+                Type = bufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
+                When = when,
+                PositionTicks = positionTicks
+            };
+            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Update session ping.
+        /// </summary>
+        /// <param name="ping">The ping.</param>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpPost]
+        public ActionResult Ping([FromQuery] double ping)
+        {
+            var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+            var syncPlayRequest = new PlaybackRequest()
+            {
+                Type = PlaybackRequestType.UpdatePing,
+                Ping = Convert.ToInt64(ping)
+            };
+            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+            return NoContent();
+        }
+    }
+}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
new file mode 100644
index 0000000000..57a720b26c
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Globalization;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The time sync controller.
+    /// </summary>
+    [Route("/GetUtcTime")]
+    public class TimeSyncController : BaseJellyfinApiController
+    {
+        /// <summary>
+        /// Gets the current utc time.
+        /// </summary>
+        /// <response code="200">Time returned.</response>
+        /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
+        [HttpGet]
+        [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
+        public ActionResult<UtcTimeResponse> GetUtcTime()
+        {
+            // Important to keep the following line at the beginning
+            var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
+
+            var response = new UtcTimeResponse();
+            response.RequestReceptionTime = requestReceptionTime;
+
+            // Important to keep the following two lines at the end
+            var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
+            response.ResponseTransmissionTime = responseTransmissionTime;
+
+            // Implementing NTP on such a high level results in this useless
+            // information being sent. On the other hand it enables future additions.
+            return response;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
deleted file mode 100644
index 1e14ea552c..0000000000
--- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System.Threading;
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.SyncPlay;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.SyncPlay
-{
-    [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")]
-    [Authenticated]
-    public class SyncPlayNewGroup : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")]
-    [Authenticated]
-    public class SyncPlayJoinGroup : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the Group id.
-        /// </summary>
-        /// <value>The Group id to join.</value>
-        [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string GroupId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the playing item id.
-        /// </summary>
-        /// <value>The client's currently playing item id.</value>
-        [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlayingItemId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")]
-    [Authenticated]
-    public class SyncPlayLeaveGroup : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")]
-    [Authenticated]
-    public class SyncPlayListGroups : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the filter item id.
-        /// </summary>
-        /// <value>The filter item id.</value>
-        [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string FilterItemId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")]
-    [Authenticated]
-    public class SyncPlayPlayRequest : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")]
-    [Authenticated]
-    public class SyncPlayPauseRequest : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")]
-    [Authenticated]
-    public class SyncPlaySeekRequest : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
-        public long PositionTicks { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
-    [Authenticated]
-    public class SyncPlayBufferingRequest : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the date used to pin PositionTicks in time.
-        /// </summary>
-        /// <value>The date related to PositionTicks.</value>
-        [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string When { get; set; }
-
-        [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
-        public long PositionTicks { get; set; }
-
-        /// <summary>
-        /// Gets or sets whether this is a buffering or a buffering-done request.
-        /// </summary>
-        /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
-        [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool BufferingDone { get; set; }
-    }
-
-    [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")]
-    [Authenticated]
-    public class SyncPlayUpdatePing : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
-        public double Ping { get; set; }
-    }
-
-    /// <summary>
-    /// Class SyncPlayService.
-    /// </summary>
-    public class SyncPlayService : BaseApiService
-    {
-        /// <summary>
-        /// The session context.
-        /// </summary>
-        private readonly ISessionContext _sessionContext;
-
-        /// <summary>
-        /// The SyncPlay manager.
-        /// </summary>
-        private readonly ISyncPlayManager _syncPlayManager;
-
-        public SyncPlayService(
-            ILogger<SyncPlayService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISessionContext sessionContext,
-            ISyncPlayManager syncPlayManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _sessionContext = sessionContext;
-            _syncPlayManager = syncPlayManager;
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayNewGroup request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayJoinGroup request)
-        {
-            var currentSession = GetSession(_sessionContext);
-
-            Guid groupId;
-            Guid playingItemId = Guid.Empty;
-
-            if (!Guid.TryParse(request.GroupId, out groupId))
-            {
-                Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
-                return;
-            }
-
-            // Both null and empty strings mean that client isn't playing anything
-            if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId))
-            {
-                Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
-                return;
-            }
-
-            var joinRequest = new JoinGroupRequest()
-            {
-                GroupId = groupId,
-                PlayingItemId = playingItemId
-            };
-
-            _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayLeaveGroup request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <value>The requested list of groups.</value>
-        public List<GroupInfoView> Post(SyncPlayListGroups request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var filterItemId = Guid.Empty;
-
-            if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId))
-            {
-                Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId);
-            }
-
-            return _syncPlayManager.ListGroups(currentSession, filterItemId);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayPlayRequest request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var syncPlayRequest = new PlaybackRequest()
-            {
-                Type = PlaybackRequestType.Play
-            };
-            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayPauseRequest request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var syncPlayRequest = new PlaybackRequest()
-            {
-                Type = PlaybackRequestType.Pause
-            };
-            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlaySeekRequest request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var syncPlayRequest = new PlaybackRequest()
-            {
-                Type = PlaybackRequestType.Seek,
-                PositionTicks = request.PositionTicks
-            };
-            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayBufferingRequest request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var syncPlayRequest = new PlaybackRequest()
-            {
-                Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
-                When = DateTime.Parse(request.When),
-                PositionTicks = request.PositionTicks
-            };
-            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(SyncPlayUpdatePing request)
-        {
-            var currentSession = GetSession(_sessionContext);
-            var syncPlayRequest = new PlaybackRequest()
-            {
-                Type = PlaybackRequestType.UpdatePing,
-                Ping = Convert.ToInt64(request.Ping)
-            };
-            _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs
deleted file mode 100644
index 4a9307e62f..0000000000
--- a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.SyncPlay;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.SyncPlay
-{
-    [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")]
-    public class GetUtcTime : IReturnVoid
-    {
-        // Nothing
-    }
-
-    /// <summary>
-    /// Class TimeSyncService.
-    /// </summary>
-    public class TimeSyncService : BaseApiService
-    {
-        public TimeSyncService(
-            ILogger<TimeSyncService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            // Do nothing
-        }
-
-        /// <summary>
-        /// Handles the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <value>The current UTC time response.</value>
-        public UtcTimeResponse Get(GetUtcTime request)
-        {
-            // Important to keep the following line at the beginning
-            var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
-
-            var response = new UtcTimeResponse();
-            response.RequestReceptionTime = requestReceptionTime;
-
-            // Important to keep the following two lines at the end
-            var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
-            response.ResponseTransmissionTime = responseTransmissionTime;
-
-            // Implementing NTP on such a high level results in this useless
-            // information being sent. On the other hand it enables future additions.
-            return response;
-        }
-    }
-}

From bb2e90c1e1f642af55ea57133dc625614d7eef16 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 15 Jul 2020 16:40:49 +0200
Subject: [PATCH 305/463] Remove unused api files

---
 MediaBrowser.Api/Devices/DeviceService.cs     | 104 ----
 MediaBrowser.Api/MediaBrowser.Api.csproj      |   4 -
 MediaBrowser.Api/Movies/MoviesService.cs      |  96 ----
 MediaBrowser.Api/SimilarItemsHelper.cs        | 223 --------
 .../UserLibrary/BaseItemsByNameService.cs     | 388 --------------
 .../UserLibrary/BaseItemsRequest.cs           | 475 ------------------
 6 files changed, 1290 deletions(-)
 delete mode 100644 MediaBrowser.Api/Devices/DeviceService.cs
 delete mode 100644 MediaBrowser.Api/Movies/MoviesService.cs
 delete mode 100644 MediaBrowser.Api/SimilarItemsHelper.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs
deleted file mode 100644
index dd3f3e738a..0000000000
--- a/MediaBrowser.Api/Devices/DeviceService.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Devices
-{
-    [Route("/Devices", "GET", Summary = "Gets all devices")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDevices : DeviceQuery, IReturn<QueryResult<DeviceInfo>>
-    {
-    }
-
-    [Route("/Devices/Info", "GET", Summary = "Gets info for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceInfo : IReturn<DeviceInfo>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/Options", "GET", Summary = "Gets options for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceOptions : IReturn<DeviceOptions>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices", "DELETE", Summary = "Deletes a device")]
-    public class DeleteDevice
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/Options", "POST", Summary = "Updates device options")]
-    [Authenticated(Roles = "Admin")]
-    public class PostDeviceOptions : DeviceOptions, IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    public class DeviceService : BaseApiService
-    {
-        private readonly IDeviceManager _deviceManager;
-        private readonly IAuthenticationRepository _authRepo;
-        private readonly ISessionManager _sessionManager;
-
-        public DeviceService(
-            ILogger<DeviceService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDeviceManager deviceManager,
-            IAuthenticationRepository authRepo,
-            ISessionManager sessionManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _deviceManager = deviceManager;
-            _authRepo = authRepo;
-            _sessionManager = sessionManager;
-        }
-
-        public void Post(PostDeviceOptions request)
-        {
-            _deviceManager.UpdateDeviceOptions(request.Id, request);
-        }
-
-        public object Get(GetDevices request)
-        {
-            return ToOptimizedResult(_deviceManager.GetDevices(request));
-        }
-
-        public object Get(GetDeviceInfo request)
-        {
-            return _deviceManager.GetDevice(request.Id);
-        }
-
-        public object Get(GetDeviceOptions request)
-        {
-            return _deviceManager.GetDeviceOptions(request.Id);
-        }
-
-        public void Delete(DeleteDevice request)
-        {
-            var sessions = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                DeviceId = request.Id
-
-            }).Items;
-
-            foreach (var session in sessions)
-            {
-                _sessionManager.Logout(session);
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index cd329c94f9..d703bdb058 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -14,10 +14,6 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Library" />
-  </ItemGroup>
-
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
deleted file mode 100644
index 1931914d2b..0000000000
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-
-namespace MediaBrowser.Api.Movies
-{
-    /// <summary>
-    /// Class MoviesService
-    /// </summary>
-    [Authenticated]
-    public class MoviesService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager
-        /// </summary>
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MoviesService" /> class.
-        /// </summary>
-        public MoviesService(
-            ILogger<MoviesService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                IncludeItemTypes = itemTypes.ToArray(),
-                IsMovie = true,
-                SimilarTo = item,
-                EnableGroupByMetadataKey = true,
-                DtoOptions = dtoOptions
-
-            });
-
-            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnList,
-
-                TotalRecordCount = itemsResult.Count
-            };
-
-            return result;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs
deleted file mode 100644
index dcd22280a6..0000000000
--- a/MediaBrowser.Api/SimilarItemsHelper.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class BaseGetSimilarItemsFromItem
-    /// </summary>
-    public class BaseGetSimilarItemsFromItem : BaseGetSimilarItems
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        public string ExcludeArtistIds { get; set; }
-    }
-
-    public class BaseGetSimilarItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-    }
-
-    /// <summary>
-    /// Class SimilarItemsHelper
-    /// </summary>
-    public static class SimilarItemsHelper
-    {
-        internal static QueryResult<BaseItemDto> GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
-                libraryManager.RootFolder) : libraryManager.GetItemById(request.Id);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
-                Recursive = true,
-                DtoOptions = dtoOptions
-            };
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
-            {
-                query.ExcludeArtistIds = BaseApiService.GetGuids(request.ExcludeArtistIds);
-            }
-
-            var inputItems = libraryManager.GetItemList(query);
-
-            var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
-                .ToList();
-
-            var returnItems = items;
-
-            if (request.Limit.HasValue)
-            {
-                returnItems = returnItems.Take(request.Limit.Value).ToList();
-            }
-
-            var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-
-                TotalRecordCount = items.Count
-            };
-        }
-
-        /// <summary>
-        /// Gets the similaritems.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="inputItems">The input items.</param>
-        /// <param name="getSimilarityScore">The get similarity score.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        internal static IEnumerable<BaseItem> GetSimilaritems(BaseItem item, ILibraryManager libraryManager, IEnumerable<BaseItem> inputItems, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
-        {
-            var itemId = item.Id;
-            inputItems = inputItems.Where(i => i.Id != itemId);
-            var itemPeople = libraryManager.GetPeople(item);
-            var allPeople = libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                AppearsInItemId = item.Id
-            });
-
-            return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, itemPeople, allPeople, i)))
-                .Where(i => i.Item2 > 2)
-                .OrderByDescending(i => i.Item2)
-                .Select(i => i.Item1);
-        }
-
-        private static IEnumerable<string> GetTags(BaseItem item)
-        {
-            return item.Tags;
-        }
-
-        /// <summary>
-        /// Gets the similiarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item1People">The item1 people.</param>
-        /// <param name="allPeople">All people.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
-        {
-            var points = 0;
-
-            if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
-            {
-                points += 10;
-            }
-
-            // Find common genres
-            points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
-            // Find common tags
-            points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
-            // Find common studios
-            points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
-
-            var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
-                .Select(i => i.Name)
-                .Where(i => !string.IsNullOrWhiteSpace(i))
-                .DistinctNames()
-                .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
-            {
-                if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 5;
-                }
-                if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 2;
-                }
-
-                return 1;
-            });
-
-            if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue)
-            {
-                var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value);
-
-                // Add if they came out within the same decade
-                if (diff < 10)
-                {
-                    points += 2;
-                }
-
-                // And more if within five years
-                if (diff < 5)
-                {
-                    points += 2;
-                }
-            }
-
-            return points;
-        }
-
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
deleted file mode 100644
index a1ec084679..0000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ /dev/null
@@ -1,388 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class BaseItemsByNameService
-    /// </summary>
-    /// <typeparam name="TItemType">The type of the T item type.</typeparam>
-    public abstract class BaseItemsByNameService<TItemType> : BaseApiService
-        where TItemType : BaseItem, IItemByName
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BaseItemsByNameService{TItemType}" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="userDataRepository">The user data repository.</param>
-        /// <param name="dtoService">The dto service.</param>
-        protected BaseItemsByNameService(
-            ILogger<BaseItemsByNameService<TItemType>> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            UserManager = userManager;
-            LibraryManager = libraryManager;
-            UserDataRepository = userDataRepository;
-            DtoService = dtoService;
-            AuthorizationContext = authorizationContext;
-        }
-
-        /// <summary>
-        /// Gets the _user manager.
-        /// </summary>
-        protected IUserManager UserManager { get; }
-
-        /// <summary>
-        /// Gets the library manager
-        /// </summary>
-        protected ILibraryManager LibraryManager { get; }
-
-        protected IUserDataManager UserDataRepository { get; }
-
-        protected IDtoService DtoService { get; }
-
-        protected IAuthorizationContext AuthorizationContext { get; }
-
-        protected BaseItem GetParentItem(GetItemsByName request)
-        {
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            return parentItem;
-        }
-
-        protected string GetParentItemViewType(GetItemsByName request)
-        {
-            var parent = GetParentItem(request);
-
-            if (parent is IHasCollectionType collectionFolder)
-            {
-                return collectionFolder.CollectionType;
-            }
-
-            return null;
-        }
-
-        protected QueryResult<BaseItemDto> GetResultSlim(GetItemsByName request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            User user = null;
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            var excludeItemTypes = request.GetExcludeItemTypes();
-            var includeItemTypes = request.GetIncludeItemTypes();
-            var mediaTypes = request.GetMediaTypes();
-
-            var query = new InternalItemsQuery(user)
-            {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
-                MediaTypes = mediaTypes,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                IsFavorite = request.IsFavorite,
-                NameLessThan = request.NameLessThan,
-                NameStartsWith = request.NameStartsWith,
-                NameStartsWithOrGreater = request.NameStartsWithOrGreater,
-                Tags = request.GetTags(),
-                OfficialRatings = request.GetOfficialRatings(),
-                Genres = request.GetGenres(),
-                GenreIds = GetGuids(request.GenreIds),
-                StudioIds = GetGuids(request.StudioIds),
-                Person = request.Person,
-                PersonIds = GetGuids(request.PersonIds),
-                PersonTypes = request.GetPersonTypes(),
-                Years = request.GetYears(),
-                MinCommunityRating = request.MinCommunityRating,
-                DtoOptions = dtoOptions,
-                SearchTerm = request.SearchTerm,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            if (!string.IsNullOrWhiteSpace(request.ParentId))
-            {
-                if (parentItem is Folder)
-                {
-                    query.AncestorIds = new[] { new Guid(request.ParentId) };
-                }
-                else
-                {
-                    query.ItemIds = new[] { new Guid(request.ParentId) };
-                }
-            }
-
-            // Studios
-            if (!string.IsNullOrEmpty(request.Studios))
-            {
-                query.StudioIds = request.Studios.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return LibraryManager.GetStudio(i);
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = GetItems(request, query);
-
-            var dtos = result.Items.Select(i =>
-            {
-                var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user);
-
-                if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes))
-                {
-                    SetItemCounts(dto, i.Item2);
-                }
-                return dto;
-            });
-
-            return new QueryResult<BaseItemDto>
-            {
-                Items = dtos.ToArray(),
-                TotalRecordCount = result.TotalRecordCount
-            };
-        }
-
-        protected virtual QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return new QueryResult<(BaseItem, ItemCounts)>();
-        }
-
-        private void SetItemCounts(BaseItemDto dto, ItemCounts counts)
-        {
-            dto.ChildCount = counts.ItemCount;
-            dto.ProgramCount = counts.ProgramCount;
-            dto.SeriesCount = counts.SeriesCount;
-            dto.EpisodeCount = counts.EpisodeCount;
-            dto.MovieCount = counts.MovieCount;
-            dto.TrailerCount = counts.TrailerCount;
-            dto.AlbumCount = counts.AlbumCount;
-            dto.SongCount = counts.SongCount;
-            dto.ArtistCount = counts.ArtistCount;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{ItemsResult}.</returns>
-        protected QueryResult<BaseItemDto> GetResult(GetItemsByName request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            User user = null;
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            IList<BaseItem> items;
-
-            var excludeItemTypes = request.GetExcludeItemTypes();
-            var includeItemTypes = request.GetIncludeItemTypes();
-            var mediaTypes = request.GetMediaTypes();
-
-            var query = new InternalItemsQuery(user)
-            {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
-                MediaTypes = mediaTypes,
-                DtoOptions = dtoOptions
-            };
-
-            bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
-
-            if (parentItem.IsFolder)
-            {
-                var folder = (Folder)parentItem;
-
-                if (!request.UserId.Equals(Guid.Empty))
-                {
-                    items = request.Recursive ?
-                        folder.GetRecursiveChildren(user, query).ToList() :
-                        folder.GetChildren(user, true).Where(Filter).ToList();
-                }
-                else
-                {
-                    items = request.Recursive ?
-                        folder.GetRecursiveChildren(Filter) :
-                        folder.Children.Where(Filter).ToList();
-                }
-            }
-            else
-            {
-                items = new[] { parentItem }.Where(Filter).ToList();
-            }
-
-            var extractedItems = GetAllItems(request, items);
-
-            var filteredItems = LibraryManager.Sort(extractedItems, user, request.GetOrderBy());
-
-            var ibnItemsArray = filteredItems.ToList();
-
-            IEnumerable<BaseItem> ibnItems = ibnItemsArray;
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = ibnItemsArray.Count
-            };
-
-            if (request.StartIndex.HasValue || request.Limit.HasValue)
-            {
-                if (request.StartIndex.HasValue)
-                {
-                    ibnItems = ibnItems.Skip(request.StartIndex.Value);
-                }
-
-                if (request.Limit.HasValue)
-                {
-                    ibnItems = ibnItems.Take(request.Limit.Value);
-                }
-
-            }
-
-            var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
-
-            var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
-
-            result.Items = dtos.Where(i => i != null).ToArray();
-
-            return result;
-        }
-
-        /// <summary>
-        /// Filters the items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="f">The f.</param>
-        /// <param name="excludeItemTypes">The exclude item types.</param>
-        /// <param name="includeItemTypes">The include item types.</param>
-        /// <param name="mediaTypes">The media types.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes)
-        {
-            // Exclude item types
-            if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            // Include item types
-            if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            // Include MediaTypes
-            if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Task{`0}}.</returns>
-        protected abstract IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items);
-    }
-
-    /// <summary>
-    /// Class GetItemsByName
-    /// </summary>
-    public class GetItemsByName : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-        public GetItemsByName()
-        {
-            Recursive = true;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
deleted file mode 100644
index 7561b5c892..0000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
+++ /dev/null
@@ -1,475 +0,0 @@
-using System;
-using System.Linq;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    public abstract class BaseItemsRequest : IHasDtoOptions
-    {
-        protected BaseItemsRequest()
-        {
-            EnableImages = true;
-            EnableTotalRecordCount = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the max offical rating.
-        /// </summary>
-        /// <value>The max offical rating.</value>
-        [ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MaxOfficialRating { get; set; }
-
-        [ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasThemeSong { get; set; }
-
-        [ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasThemeVideo { get; set; }
-
-        [ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasSubtitles { get; set; }
-
-        [ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasSpecialFeature { get; set; }
-
-        [ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasTrailer { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MinIndexNumber { get; set; }
-
-        [ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ParentIndexNumber { get; set; }
-
-        [ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasParentalRating { get; set; }
-
-        [ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHD { get; set; }
-
-        public bool? Is4K { get; set; }
-
-        [ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string LocationTypes { get; set; }
-
-        [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeLocationTypes { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsUnaired { get; set; }
-
-        [ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double? MinCommunityRating { get; set; }
-
-        [ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double? MinCriticRating { get; set; }
-
-        [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AiredDuringSeason { get; set; }
-
-        [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinPremiereDate { get; set; }
-
-        [ApiMember(Name = "MinDateLastSaved", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDateLastSaved { get; set; }
-
-        [ApiMember(Name = "MinDateLastSavedForUser", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDateLastSavedForUser { get; set; }
-
-        [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MaxPremiereDate { get; set; }
-
-        [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasOverview { get; set; }
-
-        [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasImdbId { get; set; }
-
-        [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasTmdbId { get; set; }
-
-        [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasTvdbId { get; set; }
-
-        [ApiMember(Name = "ExcludeItemIds", Description = "Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemIds { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Whether or not to perform the query recursively
-        /// </summary>
-        /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "Recursive", Description = "When searching within folders, this determines whether or not the search will be recursive. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool Recursive { get; set; }
-
-        public string SearchTerm { get; set; }
-
-        /// <summary>
-        /// Gets or sets the sort order.
-        /// </summary>
-        /// <value>The sort order.</value>
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Gets or sets the exclude item types.
-        /// </summary>
-        /// <value>The exclude item types.</value>
-        [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the include item types.
-        /// </summary>
-        /// <value>The include item types.</value>
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        /// <summary>
-        /// Filters to apply to the results
-        /// </summary>
-        /// <value>The filters.</value>
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        /// <summary>
-        /// Gets or sets the Isfavorite option
-        /// </summary>
-        /// <value>IsFavorite</value>
-        [ApiMember(Name = "IsFavorite", Description = "Optional filter by items that are marked as favorite, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-
-        /// <summary>
-        /// Gets or sets the media types.
-        /// </summary>
-        /// <value>The media types.</value>
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the image types.
-        /// </summary>
-        /// <value>The image types.</value>
-        [ApiMember(Name = "ImageTypes", Description = "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ImageTypes { get; set; }
-
-        /// <summary>
-        /// What to sort the results by
-        /// </summary>
-        /// <value>The sort by.</value>
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "IsPlayed", Description = "Optional filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlayed { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific genres
-        /// </summary>
-        /// <value>The genres.</value>
-        [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Genres { get; set; }
-
-        public string GenreIds { get; set; }
-
-        [ApiMember(Name = "OfficialRatings", Description = "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string OfficialRatings { get; set; }
-
-        [ApiMember(Name = "Tags", Description = "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Tags { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific years
-        /// </summary>
-        /// <value>The years.</value>
-        [ApiMember(Name = "Years", Description = "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Years { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing a specific person
-        /// </summary>
-        /// <value>The person.</value>
-        [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Person { get; set; }
-
-        [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string PersonIds { get; set; }
-
-        /// <summary>
-        /// If the Person filter is used, this can also be used to restrict to a specific person type
-        /// </summary>
-        /// <value>The type of the person.</value>
-        [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string PersonTypes { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific studios
-        /// </summary>
-        /// <value>The studios.</value>
-        [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Studios { get; set; }
-
-        [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string StudioIds { get; set; }
-
-        /// <summary>
-        /// Gets or sets the studios.
-        /// </summary>
-        /// <value>The studios.</value>
-        [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Artists { get; set; }
-
-        public string ExcludeArtistIds { get; set; }
-
-        [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ArtistIds { get; set; }
-
-        public string AlbumArtistIds { get; set; }
-
-        public string ContributingArtistIds { get; set; }
-
-        [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Albums { get; set; }
-
-        public string AlbumIds { get; set; }
-
-        /// <summary>
-        /// Gets or sets the item ids.
-        /// </summary>
-        /// <value>The item ids.</value>
-        [ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Ids { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video types.
-        /// </summary>
-        /// <value>The video types.</value>
-        [ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string VideoTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the min offical rating.
-        /// </summary>
-        /// <value>The min offical rating.</value>
-        [ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinOfficialRating { get; set; }
-
-        [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? IsLocked { get; set; }
-
-        [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlaceHolder { get; set; }
-
-        [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasOfficialRating { get; set; }
-
-        [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? CollapseBoxSetItems { get; set; }
-
-        public int? MinWidth { get; set; }
-        public int? MinHeight { get; set; }
-        public int? MaxWidth { get; set; }
-        public int? MaxHeight { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video formats.
-        /// </summary>
-        /// <value>The video formats.</value>
-        [ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? Is3D { get; set; }
-
-        /// <summary>
-        /// Gets or sets the series status.
-        /// </summary>
-        /// <value>The series status.</value>
-        [ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SeriesStatus { get; set; }
-
-        [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameStartsWithOrGreater { get; set; }
-
-        [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameStartsWith { get; set; }
-
-        [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameLessThan { get; set; }
-
-        public string[] GetGenres()
-        {
-            return (Genres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetTags()
-        {
-            return (Tags ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetOfficialRatings()
-        {
-            return (OfficialRatings ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetExcludeItemTypes()
-        {
-            return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public int[] GetYears()
-        {
-            return (Years ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
-        }
-
-        public string[] GetStudios()
-        {
-            return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetPersonTypes()
-        {
-            return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public VideoType[] GetVideoTypes()
-        {
-            return string.IsNullOrEmpty(VideoTypes)
-                ? Array.Empty<VideoType>()
-                : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                    .Select(v => Enum.Parse<VideoType>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public ItemFilter[] GetFilters()
-        {
-            var val = Filters;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ItemFilter>()
-                : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the image types.
-        /// </summary>
-        /// <returns>IEnumerable{ImageType}.</returns>
-        public ImageType[] GetImageTypes()
-        {
-            var val = ImageTypes;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ImageType>()
-                : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public ValueTuple<string, SortOrder>[] GetOrderBy()
-        {
-            return GetOrderBy(SortBy, SortOrder);
-        }
-
-        public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
-        {
-            var val = sortBy;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return Array.Empty<ValueTuple<string, SortOrder>>();
-            }
-
-            var vals = val.Split(',');
-            if (string.IsNullOrWhiteSpace(requestedSortOrder))
-            {
-                requestedSortOrder = "Ascending";
-            }
-
-            var sortOrders = requestedSortOrder.Split(',');
-
-            var result = new ValueTuple<string, SortOrder>[vals.Length];
-
-            for (var i = 0; i < vals.Length; i++)
-            {
-                var sortOrderIndex = sortOrders.Length > i ? i : 0;
-
-                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
-                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
-                    ? MediaBrowser.Model.Entities.SortOrder.Descending
-                    : MediaBrowser.Model.Entities.SortOrder.Ascending;
-
-                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
-            }
-
-            return result;
-        }
-    }
-}

From ab396225eaf486932fdb2f23eefa1cbfecbb27f4 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Tue, 30 Jun 2020 21:44:41 -0400
Subject: [PATCH 306/463] Migrate Display Preferences to EF Core

---
 Emby.Dlna/ContentDirectory/ControlHandler.cs  |   1 +
 .../ApplicationHost.cs                        |   3 -
 .../Channels/ChannelManager.cs                |   1 +
 .../SqliteDisplayPreferencesRepository.cs     | 225 ----------
 .../Data/SqliteItemRepository.cs              |   1 +
 .../Emby.Server.Implementations.csproj        |   2 +-
 .../Images/CollectionFolderImageProvider.cs   |   2 +-
 .../Images/FolderImageProvider.cs             |   2 +-
 .../Images/GenreImageProvider.cs              |   1 +
 .../Library/LibraryManager.cs                 |   1 -
 .../Library/MusicManager.cs                   |   2 +-
 .../Library/SearchEngine.cs                   |   2 +-
 .../LiveTv/EmbyTV/EmbyTV.cs                   |   1 +
 Jellyfin.Data/Entities/DisplayPreferences.cs  |  72 ++++
 Jellyfin.Data/Entities/HomeSection.cs         |  21 +
 Jellyfin.Data/Entities/User.cs                |   5 +
 Jellyfin.Data/Enums/HomeSectionType.cs        |  53 +++
 Jellyfin.Data/Enums/IndexingKind.cs           |  20 +
 .../Enums}/ScrollDirection.cs                 |   8 +-
 .../Enums}/SortOrder.cs                       |   8 +-
 Jellyfin.Data/Enums/ViewType.cs               |  38 ++
 .../DisplayPreferencesManager.cs              |  49 +++
 Jellyfin.Server.Implementations/JellyfinDb.cs |   2 +
 ...30170339_AddDisplayPreferences.Designer.cs | 403 ++++++++++++++++++
 .../20200630170339_AddDisplayPreferences.cs   |  92 ++++
 .../Migrations/JellyfinDbModelSnapshot.cs     |  93 +++-
 Jellyfin.Server/CoreAppHost.cs                |   2 +
 Jellyfin.Server/Migrations/MigrationRunner.cs |   3 +-
 .../Routines/MigrateDisplayPreferencesDb.cs   | 118 +++++
 MediaBrowser.Api/ChannelService.cs            |   2 +-
 MediaBrowser.Api/DisplayPreferencesService.cs |  92 +++-
 MediaBrowser.Api/Movies/MoviesService.cs      |   1 +
 MediaBrowser.Api/SuggestionsService.cs        |   1 +
 MediaBrowser.Api/TvShowsService.cs            |   1 +
 .../UserLibrary/BaseItemsRequest.cs           |   5 +-
 .../Entities/UserViewBuilder.cs               |   1 +
 .../IDisplayPreferencesManager.cs             |  25 ++
 .../Library/ILibraryManager.cs                |   1 +
 .../IDisplayPreferencesRepository.cs          |  53 ---
 MediaBrowser.Controller/Playlists/Playlist.cs |   1 +
 MediaBrowser.Model/Dlna/SortCriteria.cs       |   2 +-
 ...references.cs => DisplayPreferencesDto.cs} |  12 +-
 .../LiveTv/LiveTvChannelQuery.cs              |   2 +-
 MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs |   2 +-
 44 files changed, 1101 insertions(+), 331 deletions(-)
 delete mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
 create mode 100644 Jellyfin.Data/Entities/DisplayPreferences.cs
 create mode 100644 Jellyfin.Data/Entities/HomeSection.cs
 create mode 100644 Jellyfin.Data/Enums/HomeSectionType.cs
 create mode 100644 Jellyfin.Data/Enums/IndexingKind.cs
 rename {MediaBrowser.Model/Entities => Jellyfin.Data/Enums}/ScrollDirection.cs (53%)
 rename {MediaBrowser.Model/Entities => Jellyfin.Data/Enums}/SortOrder.cs (56%)
 create mode 100644 Jellyfin.Data/Enums/ViewType.cs
 create mode 100644 Jellyfin.Server.Implementations/DisplayPreferencesManager.cs
 create mode 100644 Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs
 create mode 100644 Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs
 create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
 create mode 100644 MediaBrowser.Controller/IDisplayPreferencesManager.cs
 delete mode 100644 MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
 rename MediaBrowser.Model/Entities/{DisplayPreferences.cs => DisplayPreferencesDto.cs} (94%)

diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 291de5245b..00821bf780 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -11,6 +11,7 @@ using System.Xml;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index f6077400d6..f6f10beb09 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -554,8 +554,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
-
             serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -650,7 +648,6 @@ namespace Emby.Server.Implementations
             _httpServer = Resolve<IHttpServer>();
             _httpClient = Resolve<IHttpClient>();
 
-            ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
             SetStaticProperties();
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index c803d9d825..2a7cddd87b 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
deleted file mode 100644
index 5597155a8d..0000000000
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    /// <summary>
-    /// Class SQLiteDisplayPreferencesRepository.
-    /// </summary>
-    public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
-    {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly JsonSerializerOptions _jsonOptions;
-
-        public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
-            : base(logger)
-        {
-            _fileSystem = fileSystem;
-
-            _jsonOptions = JsonDefaults.GetOptions();
-
-            DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
-        }
-
-        /// <summary>
-        /// Gets the name of the repository.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name => "SQLite";
-
-        public void Initialize()
-        {
-            try
-            {
-                InitializeInternal();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
-                _fileSystem.DeleteFile(DbFilePath);
-
-                InitializeInternal();
-            }
-        }
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        /// <returns>Task.</returns>
-        private void InitializeInternal()
-        {
-            string[] queries =
-            {
-                "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
-                "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
-            };
-
-            using (var connection = GetConnection())
-            {
-                connection.RunQueries(queries);
-            }
-        }
-
-        /// <summary>
-        /// Save the display preferences associated with an item in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            if (string.IsNullOrEmpty(displayPreferences.Id))
-            {
-                throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db => SaveDisplayPreferences(displayPreferences, userId, client, db),
-                    TransactionMode);
-            }
-        }
-
-        private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
-        {
-            var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
-
-            using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
-            {
-                statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
-                statement.TryBind("@userId", userId.ToByteArray());
-                statement.TryBind("@client", client);
-                statement.TryBind("@data", serialized);
-
-                statement.MoveNext();
-            }
-        }
-
-        /// <summary>
-        /// Save all display preferences associated with a user in the repo.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException(nameof(displayPreferences));
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            using (var connection = GetConnection())
-            {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        foreach (var displayPreference in displayPreferences)
-                        {
-                            SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
-                        }
-                    },
-                    TransactionMode);
-            }
-        }
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
-        {
-            if (string.IsNullOrEmpty(displayPreferencesId))
-            {
-                throw new ArgumentNullException(nameof(displayPreferencesId));
-            }
-
-            var guidId = displayPreferencesId.GetMD5();
-
-            using (var connection = GetConnection(true))
-            {
-                using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
-                {
-                    statement.TryBind("@id", guidId.ToByteArray());
-                    statement.TryBind("@userId", userId.ToByteArray());
-                    statement.TryBind("@client", client);
-
-                    foreach (var row in statement.ExecuteQuery())
-                    {
-                        return Get(row);
-                    }
-                }
-            }
-
-            return new DisplayPreferences
-            {
-                Id = guidId.ToString("N", CultureInfo.InvariantCulture)
-            };
-        }
-
-        /// <summary>
-        /// Gets all display preferences for the given user.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="ArgumentNullException">item</exception>
-        public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
-        {
-            var list = new List<DisplayPreferences>();
-
-            using (var connection = GetConnection(true))
-            using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
-            {
-                statement.TryBind("@userId", userId.ToByteArray());
-
-                foreach (var row in statement.ExecuteQuery())
-                {
-                    list.Add(Get(row));
-                }
-            }
-
-            return list;
-        }
-
-        private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
-            => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
-
-        public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
-            => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
-
-        public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
-            => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
-    }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a6390b1ef2..04e5e570f7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -9,6 +9,7 @@ using System.Text;
 using System.Text.Json;
 using System.Threading;
 using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index f7ad59c10f..548dc7085c 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -54,7 +54,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index da88b8d8ab..161b4c4528 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -3,7 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index e9523386ea..0224ab32a0 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index d2aeccdb21..1cd4cd66bd 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 77d44e1313..4690f20946 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -50,7 +50,6 @@ using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
 
 namespace Emby.Server.Implementations.Library
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 0bdc599144..877fdec86e 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 3df9cc06f1..e3e554824a 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
 using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 7b0fcbc9e5..80e09f0a34 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
new file mode 100644
index 0000000000..668030149b
--- /dev/null
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    public class DisplayPreferences
+    {
+        public DisplayPreferences(string client, Guid userId)
+        {
+            RememberIndexing = false;
+            ShowBackdrop = true;
+            Client = client;
+            UserId = userId;
+
+            HomeSections = new HashSet<HomeSection>();
+        }
+
+        protected DisplayPreferences()
+        {
+        }
+
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        [Required]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id of the associated item.
+        /// </summary>
+        /// <remarks>
+        /// This is currently unused. In the future, this will allow us to have users set
+        /// display preferences per item.
+        /// </remarks>
+        public Guid? ItemId { get; set; }
+
+        [Required]
+        [MaxLength(64)]
+        [StringLength(64)]
+        public string Client { get; set; }
+
+        [Required]
+        public bool RememberIndexing { get; set; }
+
+        [Required]
+        public bool RememberSorting { get; set; }
+
+        [Required]
+        public SortOrder SortOrder { get; set; }
+
+        [Required]
+        public bool ShowSidebar { get; set; }
+
+        [Required]
+        public bool ShowBackdrop { get; set; }
+
+        public string SortBy { get; set; }
+
+        public ViewType? ViewType { get; set; }
+
+        [Required]
+        public ScrollDirection ScrollDirection { get; set; }
+
+        public IndexingKind? IndexBy { get; set; }
+
+        public virtual ICollection<HomeSection> HomeSections { get; protected set; }
+    }
+}
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
new file mode 100644
index 0000000000..f39956a54e
--- /dev/null
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -0,0 +1,21 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    public class HomeSection
+    {
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        public int DisplayPreferencesId { get; set; }
+
+        public int Order { get; set; }
+
+        public HomeSectionType Type { get; set; }
+    }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index b89b0a8f45..d93144e3a5 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -349,6 +349,11 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
 
+        /// <summary>
+        /// Gets or sets the list of item display preferences.
+        /// </summary>
+        public virtual ICollection<DisplayPreferences> DisplayPreferences { get; protected set; }
+
         /*
         /// <summary>
         /// Gets or sets the list of groups this user is a member of.
diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs
new file mode 100644
index 0000000000..be764c5924
--- /dev/null
+++ b/Jellyfin.Data/Enums/HomeSectionType.cs
@@ -0,0 +1,53 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the different options for the home screen sections.
+    /// </summary>
+    public enum HomeSectionType
+    {
+        /// <summary>
+        /// My Media.
+        /// </summary>
+        SmallLibraryTiles = 0,
+
+        /// <summary>
+        /// My Media Small.
+        /// </summary>
+        LibraryButtons = 1,
+
+        /// <summary>
+        /// Active Recordings.
+        /// </summary>
+        ActiveRecordings = 2,
+
+        /// <summary>
+        /// Continue Watching.
+        /// </summary>
+        Resume = 3,
+
+        /// <summary>
+        /// Continue Listening.
+        /// </summary>
+        ResumeAudio = 4,
+
+        /// <summary>
+        /// Latest Media.
+        /// </summary>
+        LatestMedia = 5,
+
+        /// <summary>
+        /// Next Up.
+        /// </summary>
+        NextUp = 6,
+
+        /// <summary>
+        /// Live TV.
+        /// </summary>
+        LiveTv = 7,
+
+        /// <summary>
+        /// None.
+        /// </summary>
+        None = 8
+    }
+}
diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs
new file mode 100644
index 0000000000..c4d8e70ca6
--- /dev/null
+++ b/Jellyfin.Data/Enums/IndexingKind.cs
@@ -0,0 +1,20 @@
+namespace Jellyfin.Data.Enums
+{
+    public enum IndexingKind
+    {
+        /// <summary>
+        /// Index by the premiere date.
+        /// </summary>
+        PremiereDate,
+
+        /// <summary>
+        /// Index by the production year.
+        /// </summary>
+        ProductionYear,
+
+        /// <summary>
+        /// Index by the community rating.
+        /// </summary>
+        CommunityRating
+    }
+}
diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/Jellyfin.Data/Enums/ScrollDirection.cs
similarity index 53%
rename from MediaBrowser.Model/Entities/ScrollDirection.cs
rename to Jellyfin.Data/Enums/ScrollDirection.cs
index a1de0edcbb..382f585ba0 100644
--- a/MediaBrowser.Model/Entities/ScrollDirection.cs
+++ b/Jellyfin.Data/Enums/ScrollDirection.cs
@@ -1,17 +1,17 @@
-namespace MediaBrowser.Model.Entities
+namespace Jellyfin.Data.Enums
 {
     /// <summary>
-    /// Enum ScrollDirection.
+    /// An enum representing the axis that should be scrolled.
     /// </summary>
     public enum ScrollDirection
     {
         /// <summary>
-        /// The horizontal.
+        /// Horizontal scrolling direction.
         /// </summary>
         Horizontal,
 
         /// <summary>
-        /// The vertical.
+        /// Vertical scrolling direction.
         /// </summary>
         Vertical
     }
diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/Jellyfin.Data/Enums/SortOrder.cs
similarity index 56%
rename from MediaBrowser.Model/Entities/SortOrder.cs
rename to Jellyfin.Data/Enums/SortOrder.cs
index f3abc06f33..309fa78775 100644
--- a/MediaBrowser.Model/Entities/SortOrder.cs
+++ b/Jellyfin.Data/Enums/SortOrder.cs
@@ -1,17 +1,17 @@
-namespace MediaBrowser.Model.Entities
+namespace Jellyfin.Data.Enums
 {
     /// <summary>
-    /// Enum SortOrder.
+    /// An enum representing the sorting order.
     /// </summary>
     public enum SortOrder
     {
         /// <summary>
-        /// The ascending.
+        /// Sort in increasing order.
         /// </summary>
         Ascending,
 
         /// <summary>
-        /// The descending.
+        /// Sort in decreasing order.
         /// </summary>
         Descending
     }
diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs
new file mode 100644
index 0000000000..595429ab1b
--- /dev/null
+++ b/Jellyfin.Data/Enums/ViewType.cs
@@ -0,0 +1,38 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the type of view for a library or collection.
+    /// </summary>
+    public enum ViewType
+    {
+        /// <summary>
+        /// Shows banners.
+        /// </summary>
+        Banner = 0,
+
+        /// <summary>
+        /// Shows a list of content.
+        /// </summary>
+        List = 1,
+
+        /// <summary>
+        /// Shows poster artwork.
+        /// </summary>
+        Poster = 2,
+
+        /// <summary>
+        /// Shows poster artwork with a card containing the name and year.
+        /// </summary>
+        PosterCard = 3,
+
+        /// <summary>
+        /// Shows a thumbnail.
+        /// </summary>
+        Thumb = 4,
+
+        /// <summary>
+        /// Shows a thumbnail with a card containing the name and year.
+        /// </summary>
+        ThumbCard = 5
+    }
+}
diff --git a/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs
new file mode 100644
index 0000000000..132e74c6aa
--- /dev/null
+++ b/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+
+namespace Jellyfin.Server.Implementations
+{
+    /// <summary>
+    /// Manages the storage and retrieval of display preferences through Entity Framework.
+    /// </summary>
+    public class DisplayPreferencesManager : IDisplayPreferencesManager
+    {
+        private readonly JellyfinDbProvider _dbProvider;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
+        /// </summary>
+        /// <param name="dbProvider">The Jellyfin db provider.</param>
+        public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
+        {
+            _dbProvider = dbProvider;
+        }
+
+        /// <inheritdoc />
+        public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users.Find(userId);
+#pragma warning disable CA1307
+            var prefs = user.DisplayPreferences.FirstOrDefault(pref => string.Equals(pref.Client, client));
+
+            if (prefs == null)
+            {
+                prefs = new DisplayPreferences(client, userId);
+                user.DisplayPreferences.Add(prefs);
+            }
+
+            return prefs;
+        }
+
+        /// <inheritdoc />
+        public void SaveChanges(DisplayPreferences preferences)
+        {
+            var dbContext = _dbProvider.CreateContext();
+            dbContext.Update(preferences);
+            dbContext.SaveChanges();
+        }
+    }
+}
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 53120a763e..774970e942 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations
 
         public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
 
+        public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
+
         public virtual DbSet<ImageInfo> ImageInfos { get; set; }
 
         public virtual DbSet<Permission> Permissions { get; set; }
diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs
new file mode 100644
index 0000000000..75f9bb7a3c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs
@@ -0,0 +1,403 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDb))]
+    [Migration("20200630170339_AddDisplayPreferences")]
+    partial class AddDisplayPreferences
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasDefaultSchema("jellyfin")
+                .HasAnnotation("ProductVersion", "3.1.5");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Permission_Permissions_Guid");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Preference_Preferences_Guid");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("Permission_Permissions_Guid");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("Preference_Preferences_Guid");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs
new file mode 100644
index 0000000000..e9a493d9db
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs
@@ -0,0 +1,92 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class AddDisplayPreferences : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "DisplayPreferences",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: false),
+                    ItemId = table.Column<Guid>(nullable: true),
+                    Client = table.Column<string>(maxLength: 64, nullable: false),
+                    RememberIndexing = table.Column<bool>(nullable: false),
+                    RememberSorting = table.Column<bool>(nullable: false),
+                    SortOrder = table.Column<int>(nullable: false),
+                    ShowSidebar = table.Column<bool>(nullable: false),
+                    ShowBackdrop = table.Column<bool>(nullable: false),
+                    SortBy = table.Column<string>(nullable: true),
+                    ViewType = table.Column<int>(nullable: true),
+                    ScrollDirection = table.Column<int>(nullable: false),
+                    IndexBy = table.Column<int>(nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_DisplayPreferences", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_DisplayPreferences_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "HomeSection",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    DisplayPreferencesId = table.Column<int>(nullable: false),
+                    Order = table.Column<int>(nullable: false),
+                    Type = table.Column<int>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_HomeSection", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
+                        column: x => x.DisplayPreferencesId,
+                        principalSchema: "jellyfin",
+                        principalTable: "DisplayPreferences",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences",
+                column: "UserId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_HomeSection_DisplayPreferencesId",
+                schema: "jellyfin",
+                table: "HomeSection",
+                column: "DisplayPreferencesId");
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "HomeSection",
+                schema: "jellyfin");
+
+            migrationBuilder.DropTable(
+                name: "DisplayPreferences",
+                schema: "jellyfin");
+        }
+    }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 51fad82249..69b544e5ba 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.4");
+                .HasAnnotation("ProductVersion", "3.1.5");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -88,6 +88,79 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("ActivityLogs");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
                 {
                     b.Property<int>("Id")
@@ -282,6 +355,24 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsRequired();
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 207eaa98d1..c5a7368aac 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
@@ -73,6 +74,7 @@ namespace Jellyfin.Server
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
+            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 
             base.RegisterServices(serviceCollection);
         }
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index d633c554de..7f208952ce 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.MigrateActivityLogDb),
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
-            typeof(Routines.MigrateUserDb)
+            typeof(Routines.MigrateUserDb),
+            typeof(Routines.MigrateDisplayPreferencesDb)
         };
 
         /// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
new file mode 100644
index 0000000000..1ed23fe8e4
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// The migration routine for migrating the display preferences database to EF Core.
+    /// </summary>
+    public class MigrateDisplayPreferencesDb : IMigrationRoutine
+    {
+        private const string DbFilename = "displaypreferences.db";
+
+        private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
+        private readonly IServerApplicationPaths _paths;
+        private readonly JellyfinDbProvider _provider;
+        private readonly JsonSerializerOptions _jsonOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="paths">The server application paths.</param>
+        /// <param name="provider">The database provider.</param>
+        public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+        {
+            _logger = logger;
+            _paths = paths;
+            _provider = provider;
+            _jsonOptions = new JsonSerializerOptions();
+            _jsonOptions.Converters.Add(new JsonStringEnumConverter());
+        }
+
+        /// <inheritdoc />
+        public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
+
+        /// <inheritdoc />
+        public string Name => "MigrateDisplayPreferencesDatabase";
+
+        /// <inheritdoc />
+        public void Perform()
+        {
+            HomeSectionType[] defaults =
+            {
+                HomeSectionType.SmallLibraryTiles,
+                HomeSectionType.Resume,
+                HomeSectionType.ResumeAudio,
+                HomeSectionType.LiveTv,
+                HomeSectionType.NextUp,
+                HomeSectionType.LatestMedia,
+                HomeSectionType.None,
+            };
+
+            var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
+            using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
+            {
+                var dbContext = _provider.CreateContext();
+
+                var results = connection.Query("SELECT * FROM userdisplaypreferences");
+                foreach (var result in results)
+                {
+                    var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+
+                    var displayPreferences = new DisplayPreferences(result[2].ToString(), new Guid(result[1].ToBlob()))
+                    {
+                        ViewType = Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType) ? viewType : (ViewType?)null,
+                        IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
+                        ShowBackdrop = dto.ShowBackdrop,
+                        ShowSidebar = dto.ShowSidebar,
+                        SortBy = dto.SortBy,
+                        SortOrder = dto.SortOrder,
+                        RememberIndexing = dto.RememberIndexing,
+                        RememberSorting = dto.RememberSorting,
+                        ScrollDirection = dto.ScrollDirection
+                    };
+
+                    for (int i = 0; i < 7; i++)
+                    {
+                        dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
+
+                        displayPreferences.HomeSections.Add(new HomeSection
+                        {
+                            Order = i,
+                            Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
+                        });
+                    }
+
+                    dbContext.Add(displayPreferences);
+                }
+
+                dbContext.SaveChanges();
+            }
+
+            try
+            {
+                File.Move(dbFilePath, dbFilePath + ".old");
+
+                var journalPath = dbFilePath + "-journal";
+                if (File.Exists(journalPath))
+                {
+                    File.Move(journalPath, dbFilePath + ".old-journal");
+                }
+            }
+            catch (IOException e)
+            {
+                _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'");
+            }
+        }
+    }
+}
diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs
index 8c336b1c9d..8d3a9ee5a0 100644
--- a/MediaBrowser.Api/ChannelService.cs
+++ b/MediaBrowser.Api/ChannelService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Api.UserLibrary;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index c3ed40ad3c..e5dd038076 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -1,9 +1,10 @@
-using System.Threading;
+using System;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 
@@ -13,7 +14,7 @@ namespace MediaBrowser.Api
     /// Class UpdateDisplayPreferences.
     /// </summary>
     [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
-    public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
+    public class UpdateDisplayPreferences : DisplayPreferencesDto, IReturnVoid
     {
         /// <summary>
         /// Gets or sets the id.
@@ -27,7 +28,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferences>
+    public class GetDisplayPreferences : IReturn<DisplayPreferencesDto>
     {
         /// <summary>
         /// Gets or sets the id.
@@ -50,28 +51,21 @@ namespace MediaBrowser.Api
     public class DisplayPreferencesService : BaseApiService
     {
         /// <summary>
-        /// The _display preferences manager.
+        /// The user manager.
         /// </summary>
-        private readonly IDisplayPreferencesRepository _displayPreferencesManager;
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IDisplayPreferencesManager _displayPreferencesManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
         /// </summary>
-        /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="displayPreferencesManager">The display preferences manager.</param>
         public DisplayPreferencesService(
             ILogger<DisplayPreferencesService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IDisplayPreferencesRepository displayPreferencesManager)
+            IDisplayPreferencesManager displayPreferencesManager)
             : base(logger, serverConfigurationManager, httpResultFactory)
         {
-            _jsonSerializer = jsonSerializer;
             _displayPreferencesManager = displayPreferencesManager;
         }
 
@@ -81,9 +75,34 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public object Get(GetDisplayPreferences request)
         {
-            var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
+            var result = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
+
+            if (result == null)
+            {
+                return null;
+            }
+
+            var dto = new DisplayPreferencesDto
+            {
+                Client = result.Client,
+                Id = result.UserId.ToString(),
+                ViewType = result.ViewType?.ToString(),
+                SortBy = result.SortBy,
+                SortOrder = result.SortOrder,
+                IndexBy = result.IndexBy?.ToString(),
+                RememberIndexing = result.RememberIndexing,
+                RememberSorting = result.RememberSorting,
+                ScrollDirection = result.ScrollDirection,
+                ShowBackdrop = result.ShowBackdrop,
+                ShowSidebar = result.ShowSidebar
+            };
 
-            return ToOptimizedResult(result);
+            foreach (var homeSection in result.HomeSections)
+            {
+                dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
+            }
+
+            return ToOptimizedResult(dto);
         }
 
         /// <summary>
@@ -92,10 +111,43 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public void Post(UpdateDisplayPreferences request)
         {
-            // Serialize to json and then back so that the core doesn't see the request dto type
-            var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
+            HomeSectionType[] defaults =
+            {
+                HomeSectionType.SmallLibraryTiles,
+                HomeSectionType.Resume,
+                HomeSectionType.ResumeAudio,
+                HomeSectionType.LiveTv,
+                HomeSectionType.NextUp,
+                HomeSectionType.LatestMedia,
+                HomeSectionType.None,
+            };
+
+            var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
+
+            prefs.ViewType = Enum.TryParse<ViewType>(request.ViewType, true, out var viewType) ? viewType : (ViewType?)null;
+            prefs.IndexBy = Enum.TryParse<IndexingKind>(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+            prefs.ShowBackdrop = request.ShowBackdrop;
+            prefs.ShowSidebar = request.ShowSidebar;
+            prefs.SortBy = request.SortBy;
+            prefs.SortOrder = request.SortOrder;
+            prefs.RememberIndexing = request.RememberIndexing;
+            prefs.RememberSorting = request.RememberSorting;
+            prefs.ScrollDirection = request.ScrollDirection;
+            prefs.HomeSections.Clear();
+
+            for (int i = 0; i < 7; i++)
+            {
+                if (request.CustomPrefs.TryGetValue("homesection" + i, out var homeSection))
+                {
+                    prefs.HomeSections.Add(new HomeSection
+                    {
+                        Order = i,
+                        Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
+                    });
+                }
+            }
 
-            _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
+            _displayPreferencesManager.SaveChanges(prefs);
         }
     }
 }
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
index 34cccffa38..2ff322d293 100644
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ b/MediaBrowser.Api/Movies/MoviesService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs
index 17afa8e79c..b42e822e82 100644
--- a/MediaBrowser.Api/SuggestionsService.cs
+++ b/MediaBrowser.Api/SuggestionsService.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
index 165abd613d..799cea6480 100644
--- a/MediaBrowser.Api/TvShowsService.cs
+++ b/MediaBrowser.Api/TvShowsService.cs
@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
index 344861a496..fc19575b30 100644
--- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
+++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
@@ -466,8 +467,8 @@ namespace MediaBrowser.Api.UserLibrary
 
                 var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
                 var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
-                    ? MediaBrowser.Model.Entities.SortOrder.Descending
-                    : MediaBrowser.Model.Entities.SortOrder.Ascending;
+                    ? Jellyfin.Data.Enums.SortOrder.Descending
+                    : Jellyfin.Data.Enums.SortOrder.Ascending;
 
                 result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
             }
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index cb35d6e321..22bb7fd550 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
new file mode 100644
index 0000000000..e27b0ec7c3
--- /dev/null
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -0,0 +1,25 @@
+using System;
+using Jellyfin.Data.Entities;
+
+namespace MediaBrowser.Controller
+{
+    /// <summary>
+    /// Manages the storage and retrieval of display preferences.
+    /// </summary>
+    public interface IDisplayPreferencesManager
+    {
+        /// <summary>
+        /// Gets the display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user's id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>The associated display preferences.</returns>
+        DisplayPreferences GetDisplayPreferences(Guid userId, string client);
+
+        /// <summary>
+        /// Saves changes to the provided display preferences.
+        /// </summary>
+        /// <param name="preferences">The display preferences to save.</param>
+        void SaveChanges(DisplayPreferences preferences);
+    }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 9d6646857e..b5eec18463 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
deleted file mode 100644
index c2dcb66d7c..0000000000
--- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
-    /// <summary>
-    /// Interface IDisplayPreferencesRepository.
-    /// </summary>
-    public interface IDisplayPreferencesRepository : IRepository
-    {
-        /// <summary>
-        /// Saves display preferences for an item.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        void SaveDisplayPreferences(
-            DisplayPreferences displayPreferences,
-            string userId,
-            string client,
-            CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Saves all display preferences for a user.
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        void SaveAllDisplayPreferences(
-            IEnumerable<DisplayPreferences> displayPreferences,
-            Guid userId,
-            CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <param name="client">The client.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
-
-        /// <summary>
-        /// Gets all display preferences for the given user.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
-    }
-}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index b1a638883a..0fd63770f4 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 1f7fa76ade..53e4540cbb 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 namespace MediaBrowser.Model.Dlna
 {
diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
similarity index 94%
rename from MediaBrowser.Model/Entities/DisplayPreferences.cs
rename to MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
index 7e5c5be3b6..1f7fe30300 100644
--- a/MediaBrowser.Model/Entities/DisplayPreferences.cs
+++ b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
@@ -1,22 +1,18 @@
 #nullable disable
 using System.Collections.Generic;
+using Jellyfin.Data.Enums;
 
 namespace MediaBrowser.Model.Entities
 {
     /// <summary>
     /// Defines the display preferences for any item that supports them (usually Folders).
     /// </summary>
-    public class DisplayPreferences
+    public class DisplayPreferencesDto
     {
         /// <summary>
-        /// The image scale.
+        /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
         /// </summary>
-        private const double ImageScale = .9;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
-        /// </summary>
-        public DisplayPreferences()
+        public DisplayPreferencesDto()
         {
             RememberIndexing = false;
             PrimaryImageHeight = 250;
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index 2b2377fdaf..ab74aff28b 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -2,7 +2,7 @@
 #pragma warning disable CS1591
 
 using System;
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 namespace MediaBrowser.Model.LiveTv
 {
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
index b899a464b4..dae885775c 100644
--- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
@@ -1,6 +1,6 @@
 #pragma warning disable CS1591
 
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
 
 namespace MediaBrowser.Model.LiveTv
 {

From e96a36512a542168e3d50609b1c4058e965a9791 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Thu, 9 Jul 2020 22:00:34 -0400
Subject: [PATCH 307/463] Document DisplayPreferences.cs

---
 Jellyfin.Data/Entities/DisplayPreferences.cs | 77 ++++++++++++++++++++
 1 file changed, 77 insertions(+)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 668030149b..928407e7a0 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -6,8 +6,16 @@ using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
+    /// <summary>
+    /// An entity representing a user's display preferences.
+    /// </summary>
     public class DisplayPreferences
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+        /// </summary>
+        /// <param name="client">The client string.</param>
+        /// <param name="userId">The user's id.</param>
         public DisplayPreferences(string client, Guid userId)
         {
             RememberIndexing = false;
@@ -18,14 +26,29 @@ namespace Jellyfin.Data.Entities
             HomeSections = new HashSet<HomeSection>();
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+        /// </summary>
         protected DisplayPreferences()
         {
         }
 
+        /// <summary>
+        /// Gets or sets the Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
+        /// <summary>
+        /// Gets or sets the user Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public Guid UserId { get; set; }
 
@@ -38,35 +61,89 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         public Guid? ItemId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the client string.
+        /// </summary>
+        /// <remarks>
+        /// Required. Max Length = 64.
+        /// </remarks>
         [Required]
         [MaxLength(64)]
         [StringLength(64)]
         public string Client { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the indexing should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool RememberIndexing { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the sorting type should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool RememberSorting { get; set; }
 
+        /// <summary>
+        /// Gets or sets the sort order.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public SortOrder SortOrder { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to show the sidebar.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool ShowSidebar { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to show the backdrop.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public bool ShowBackdrop { get; set; }
 
+        /// <summary>
+        /// Gets or sets what the view should be sorted by.
+        /// </summary>
         public string SortBy { get; set; }
 
+        /// <summary>
+        /// Gets or sets the view type.
+        /// </summary>
         public ViewType? ViewType { get; set; }
 
+        /// <summary>
+        /// Gets or sets the scroll direction.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         [Required]
         public ScrollDirection ScrollDirection { get; set; }
 
+        /// <summary>
+        /// Gets or sets what the view should be indexed by.
+        /// </summary>
         public IndexingKind? IndexBy { get; set; }
 
+        /// <summary>
+        /// Gets or sets the home sections.
+        /// </summary>
         public virtual ICollection<HomeSection> HomeSections { get; protected set; }
     }
 }

From 3c7eb6b324ba9cf20244f136ee743bef91d4a8f3 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Thu, 9 Jul 2020 22:28:59 -0400
Subject: [PATCH 308/463] Document HomeSection.cs

---
 Jellyfin.Data/Entities/HomeSection.cs | 21 +++++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

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

From f3263c7d8ef9aa284c52fa6bd68adc1ed7c435da Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 11 Jul 2020 14:11:55 -0400
Subject: [PATCH 309/463] Remove limit of 7 home sections

---
 MediaBrowser.Api/DisplayPreferencesService.cs | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index e5dd038076..969bf564cc 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -1,10 +1,12 @@
 using System;
+using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 
@@ -135,16 +137,19 @@ namespace MediaBrowser.Api
             prefs.ScrollDirection = request.ScrollDirection;
             prefs.HomeSections.Clear();
 
-            for (int i = 0; i < 7; i++)
+            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))
             {
-                if (request.CustomPrefs.TryGetValue("homesection" + i, out var homeSection))
+                var order = int.Parse(key.Substring("homesection".Length));
+                if (!Enum.TryParse<HomeSectionType>(request.CustomPrefs[key], true, out var type))
                 {
-                    prefs.HomeSections.Add(new HomeSection
-                    {
-                        Order = i,
-                        Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
-                    });
+                    type = order < 7 ? defaults[order] : HomeSectionType.None;
                 }
+
+                prefs.HomeSections.Add(new HomeSection
+                {
+                    Order = order,
+                    Type = type
+                });
             }
 
             _displayPreferencesManager.SaveChanges(prefs);

From 817e06813efd7f4911dc54dbb497d653418ae67b Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 11 Jul 2020 14:17:33 -0400
Subject: [PATCH 310/463] Remove unused using

---
 MediaBrowser.Api/DisplayPreferencesService.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 969bf564cc..5352bb36eb 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -6,7 +6,6 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Extensions.Logging;
 

From fcfe22753749e86c4c5a0755fc6fa13ba053faf4 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Thu, 16 Jul 2020 19:05:35 -0400
Subject: [PATCH 311/463] Updated documentation to indicate required elements.

---
 Jellyfin.Data/Entities/HomeSection.cs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
index f19b6f3d41..1a59cda53a 100644
--- a/Jellyfin.Data/Entities/HomeSection.cs
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -23,16 +23,25 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Gets or sets the Id of the associated display preferences.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public int DisplayPreferencesId { get; set; }
 
         /// <summary>
         /// Gets or sets the order.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public int Order { get; set; }
 
         /// <summary>
         /// Gets or sets the type.
         /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
         public HomeSectionType Type { get; set; }
     }
 }

From 9e17db59cd3f4824dfe9e29a3a8b5267249748c0 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 12:48:22 -0400
Subject: [PATCH 312/463] Reorder HomeSectionType

---
 Jellyfin.Data/Enums/HomeSectionType.cs | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs
index be764c5924..e597c9431a 100644
--- a/Jellyfin.Data/Enums/HomeSectionType.cs
+++ b/Jellyfin.Data/Enums/HomeSectionType.cs
@@ -5,49 +5,49 @@
     /// </summary>
     public enum HomeSectionType
     {
+        /// <summary>
+        /// None.
+        /// </summary>
+        None = 0,
+
         /// <summary>
         /// My Media.
         /// </summary>
-        SmallLibraryTiles = 0,
+        SmallLibraryTiles = 1,
 
         /// <summary>
         /// My Media Small.
         /// </summary>
-        LibraryButtons = 1,
+        LibraryButtons = 2,
 
         /// <summary>
         /// Active Recordings.
         /// </summary>
-        ActiveRecordings = 2,
+        ActiveRecordings = 3,
 
         /// <summary>
         /// Continue Watching.
         /// </summary>
-        Resume = 3,
+        Resume = 4,
 
         /// <summary>
         /// Continue Listening.
         /// </summary>
-        ResumeAudio = 4,
+        ResumeAudio = 5,
 
         /// <summary>
         /// Latest Media.
         /// </summary>
-        LatestMedia = 5,
+        LatestMedia = 6,
 
         /// <summary>
         /// Next Up.
         /// </summary>
-        NextUp = 6,
+        NextUp = 7,
 
         /// <summary>
         /// Live TV.
         /// </summary>
-        LiveTv = 7,
-
-        /// <summary>
-        /// None.
-        /// </summary>
-        None = 8
+        LiveTv = 8
     }
 }

From 2831062a3f1e8d40ecf28ebef9255a40be00480a Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 14:46:17 -0400
Subject: [PATCH 313/463] Add max length for SortBy

---
 Jellyfin.Data/Entities/DisplayPreferences.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 928407e7a0..6bc6b7de1e 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -120,6 +120,8 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Gets or sets what the view should be sorted by.
         /// </summary>
+        [MaxLength(64)]
+        [StringLength(64)]
         public string SortBy { get; set; }
 
         /// <summary>

From d8060849376ae8e1cf309b04e01a24bdc6089c58 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 16:27:34 -0400
Subject: [PATCH 314/463] Remove superfluous annotations.

---
 Jellyfin.Data/Entities/DisplayPreferences.cs | 8 --------
 Jellyfin.Data/Entities/HomeSection.cs        | 1 -
 2 files changed, 9 deletions(-)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 6bc6b7de1e..6cefda7880 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -39,7 +39,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
@@ -49,7 +48,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public Guid UserId { get; set; }
 
         /// <summary>
@@ -78,7 +76,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public bool RememberIndexing { get; set; }
 
         /// <summary>
@@ -87,7 +84,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public bool RememberSorting { get; set; }
 
         /// <summary>
@@ -96,7 +92,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public SortOrder SortOrder { get; set; }
 
         /// <summary>
@@ -105,7 +100,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public bool ShowSidebar { get; set; }
 
         /// <summary>
@@ -114,7 +108,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public bool ShowBackdrop { get; set; }
 
         /// <summary>
@@ -135,7 +128,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public ScrollDirection ScrollDirection { get; set; }
 
         /// <summary>
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
index 1a59cda53a..0620462602 100644
--- a/Jellyfin.Data/Entities/HomeSection.cs
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -16,7 +16,6 @@ namespace Jellyfin.Data.Entities
         /// Identity. Required.
         /// </remarks>
         [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 

From 27eefd49f1011a9be3be4f18069942cd484c1530 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 19:36:55 -0400
Subject: [PATCH 315/463] Add missing fields

---
 Jellyfin.Data/Entities/DisplayPreferences.cs  | 32 +++++++++++++++++++
 Jellyfin.Data/Enums/ChromecastVersion.cs      | 18 +++++++++++
 ...7233541_AddDisplayPreferences.Designer.cs} | 21 +++++++++---
 ...> 20200717233541_AddDisplayPreferences.cs} | 13 ++++----
 .../Migrations/JellyfinDbModelSnapshot.cs     | 15 ++++++++-
 MediaBrowser.Api/DisplayPreferencesService.cs |  8 +++++
 6 files changed, 95 insertions(+), 12 deletions(-)
 create mode 100644 Jellyfin.Data/Enums/ChromecastVersion.cs
 rename Jellyfin.Server.Implementations/Migrations/{20200630170339_AddDisplayPreferences.Designer.cs => 20200717233541_AddDisplayPreferences.Designer.cs} (95%)
 rename Jellyfin.Server.Implementations/Migrations/{20200630170339_AddDisplayPreferences.cs => 20200717233541_AddDisplayPreferences.cs} (88%)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 6cefda7880..bcb872db37 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -135,6 +135,38 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         public IndexingKind? IndexBy { get; set; }
 
+        /// <summary>
+        /// Gets or sets the length of time to skip forwards, in milliseconds.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int SkipForwardLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the length of time to skip backwards, in milliseconds.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public int SkipBackwardLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Chromecast Version.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ChromecastVersion ChromecastVersion { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the next video info overlay should be shown.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool EnableNextVideoInfoOverlay { get; set; }
+
         /// <summary>
         /// Gets or sets the home sections.
         /// </summary>
diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
new file mode 100644
index 0000000000..c549b6acc4
--- /dev/null
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+    /// <summary>
+    /// An enum representing the version of Chromecast to be used by clients.
+    /// </summary>
+    public enum ChromecastVersion
+    {
+        /// <summary>
+        /// Stable Chromecast version.
+        /// </summary>
+        Stable,
+
+        /// <summary>
+        /// Nightly Chromecast version.
+        /// </summary>
+        Nightly
+    }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
similarity index 95%
rename from Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs
rename to Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
index 75f9bb7a3c..cf6b166172 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CS1591
-
-// <auto-generated />
+// <auto-generated />
 using System;
 using Jellyfin.Server.Implementations;
 using Microsoft.EntityFrameworkCore;
@@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace Jellyfin.Server.Implementations.Migrations
 {
     [DbContext(typeof(JellyfinDb))]
-    [Migration("20200630170339_AddDisplayPreferences")]
+    [Migration("20200717233541_AddDisplayPreferences")]
     partial class AddDisplayPreferences
     {
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -98,11 +96,17 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .ValueGeneratedOnAdd()
                         .HasColumnType("INTEGER");
 
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
                     b.Property<string>("Client")
                         .IsRequired()
                         .HasColumnType("TEXT")
                         .HasMaxLength(64);
 
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
                     b.Property<int?>("IndexBy")
                         .HasColumnType("INTEGER");
 
@@ -124,8 +128,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<bool>("ShowSidebar")
                         .HasColumnType("INTEGER");
 
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
                     b.Property<string>("SortBy")
-                        .HasColumnType("TEXT");
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
 
                     b.Property<int>("SortOrder")
                         .HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
similarity index 88%
rename from Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs
rename to Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
index e9a493d9db..3cfd02e070 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
@@ -1,7 +1,4 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1601
-
-using System;
+using System;
 using Microsoft.EntityFrameworkCore.Migrations;
 
 namespace Jellyfin.Server.Implementations.Migrations
@@ -25,10 +22,14 @@ namespace Jellyfin.Server.Implementations.Migrations
                     SortOrder = table.Column<int>(nullable: false),
                     ShowSidebar = table.Column<bool>(nullable: false),
                     ShowBackdrop = table.Column<bool>(nullable: false),
-                    SortBy = table.Column<string>(nullable: true),
+                    SortBy = table.Column<string>(maxLength: 64, nullable: true),
                     ViewType = table.Column<int>(nullable: true),
                     ScrollDirection = table.Column<int>(nullable: false),
-                    IndexBy = table.Column<int>(nullable: true)
+                    IndexBy = table.Column<int>(nullable: true),
+                    SkipForwardLength = table.Column<int>(nullable: false),
+                    SkipBackwardLength = table.Column<int>(nullable: false),
+                    ChromecastVersion = table.Column<int>(nullable: false),
+                    EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false)
                 },
                 constraints: table =>
                 {
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 69b544e5ba..76de592ac7 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -94,11 +94,17 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .ValueGeneratedOnAdd()
                         .HasColumnType("INTEGER");
 
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
                     b.Property<string>("Client")
                         .IsRequired()
                         .HasColumnType("TEXT")
                         .HasMaxLength(64);
 
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
                     b.Property<int?>("IndexBy")
                         .HasColumnType("INTEGER");
 
@@ -120,8 +126,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<bool>("ShowSidebar")
                         .HasColumnType("INTEGER");
 
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
                     b.Property<string>("SortBy")
-                        .HasColumnType("TEXT");
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
 
                     b.Property<int>("SortOrder")
                         .HasColumnType("INTEGER");
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 5352bb36eb..877b124be5 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -134,6 +134,14 @@ namespace MediaBrowser.Api
             prefs.RememberIndexing = request.RememberIndexing;
             prefs.RememberSorting = request.RememberSorting;
             prefs.ScrollDirection = request.ScrollDirection;
+            prefs.ChromecastVersion = request.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+                ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
+                : ChromecastVersion.Stable;
+            prefs.EnableNextVideoInfoOverlay = request.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+                ? bool.Parse(enableNextVideoInfoOverlay)
+                : true;
+            prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
+            prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
             prefs.HomeSections.Clear();
 
             foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))

From 1ac11863126edd570cd3f0baeebd9efb5925dde7 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 20:01:17 -0400
Subject: [PATCH 316/463] Add pragmas to DisplayPreferences migration files

---
 .../20200717233541_AddDisplayPreferences.Designer.cs         | 4 +++-
 .../Migrations/20200717233541_AddDisplayPreferences.cs       | 5 ++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
index cf6b166172..392e26daef 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
@@ -1,4 +1,6 @@
-// <auto-generated />
+#pragma warning disable CS1591
+
+// <auto-generated />
 using System;
 using Jellyfin.Server.Implementations;
 using Microsoft.EntityFrameworkCore;
diff --git a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
index 3cfd02e070..a5c344fac0 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
@@ -1,4 +1,7 @@
-using System;
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
 using Microsoft.EntityFrameworkCore.Migrations;
 
 namespace Jellyfin.Server.Implementations.Migrations

From 10f531bbe4699384439a45b8114037c42809e191 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 20:03:17 -0400
Subject: [PATCH 317/463] Manually specify enum order.

---
 Jellyfin.Data/Enums/ChromecastVersion.cs | 4 ++--
 Jellyfin.Data/Enums/IndexingKind.cs      | 6 +++---
 Jellyfin.Data/Enums/ScrollDirection.cs   | 4 ++--
 Jellyfin.Data/Enums/SortOrder.cs         | 4 ++--
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
index c549b6acc4..9211659cc3 100644
--- a/Jellyfin.Data/Enums/ChromecastVersion.cs
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -8,11 +8,11 @@
         /// <summary>
         /// Stable Chromecast version.
         /// </summary>
-        Stable,
+        Stable = 0,
 
         /// <summary>
         /// Nightly Chromecast version.
         /// </summary>
-        Nightly
+        Nightly = 2
     }
 }
diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs
index c4d8e70ca6..9badc6573b 100644
--- a/Jellyfin.Data/Enums/IndexingKind.cs
+++ b/Jellyfin.Data/Enums/IndexingKind.cs
@@ -5,16 +5,16 @@
         /// <summary>
         /// Index by the premiere date.
         /// </summary>
-        PremiereDate,
+        PremiereDate = 0,
 
         /// <summary>
         /// Index by the production year.
         /// </summary>
-        ProductionYear,
+        ProductionYear = 1,
 
         /// <summary>
         /// Index by the community rating.
         /// </summary>
-        CommunityRating
+        CommunityRating = 2
     }
 }
diff --git a/Jellyfin.Data/Enums/ScrollDirection.cs b/Jellyfin.Data/Enums/ScrollDirection.cs
index 382f585ba0..9595eb4904 100644
--- a/Jellyfin.Data/Enums/ScrollDirection.cs
+++ b/Jellyfin.Data/Enums/ScrollDirection.cs
@@ -8,11 +8,11 @@
         /// <summary>
         /// Horizontal scrolling direction.
         /// </summary>
-        Horizontal,
+        Horizontal = 0,
 
         /// <summary>
         /// Vertical scrolling direction.
         /// </summary>
-        Vertical
+        Vertical = 1
     }
 }
diff --git a/Jellyfin.Data/Enums/SortOrder.cs b/Jellyfin.Data/Enums/SortOrder.cs
index 309fa78775..760a857f51 100644
--- a/Jellyfin.Data/Enums/SortOrder.cs
+++ b/Jellyfin.Data/Enums/SortOrder.cs
@@ -8,11 +8,11 @@
         /// <summary>
         /// Sort in increasing order.
         /// </summary>
-        Ascending,
+        Ascending = 0,
 
         /// <summary>
         /// Sort in decreasing order.
         /// </summary>
-        Descending
+        Descending = 1
     }
 }

From 5993a4ac2d2c54687f015755d69d495d796163d1 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 17 Jul 2020 23:36:13 -0400
Subject: [PATCH 318/463] Fix ChromecastVersion numbering

---
 Jellyfin.Data/Enums/ChromecastVersion.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
index 9211659cc3..855c75ab45 100644
--- a/Jellyfin.Data/Enums/ChromecastVersion.cs
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -13,6 +13,6 @@
         /// <summary>
         /// Nightly Chromecast version.
         /// </summary>
-        Nightly = 2
+        Nightly = 1
     }
 }

From 3514813eb4eda997a0ea722cc2ed41979419c6dd Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sun, 12 Jul 2020 11:14:38 +0200
Subject: [PATCH 319/463] Continute work

---
 .../ApplicationHost.cs                        |   3 +
 Jellyfin.Api/Controllers/AudioController.cs   | 350 ++++++---
 .../Controllers/PlaystateController.cs        |  12 +-
 .../Helpers/FileStreamResponseHelpers.cs      | 236 ++++++
 Jellyfin.Api/Helpers/StreamingHelpers.cs      | 681 ++++++++++++++++--
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  | 453 +++++++++++-
 .../Models/{ => StreamingDtos}/StreamState.cs | 122 +++-
 .../Playback/Progressive/AudioService.cs      |   4 -
 .../MediaEncoding/EncodingHelper.cs           |  11 +
 9 files changed, 1686 insertions(+), 186 deletions(-)
 create mode 100644 Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
 rename Jellyfin.Api/Models/{ => StreamingDtos}/StreamState.cs (51%)

diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 25ee7e9ec0..c177537b89 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -46,6 +46,7 @@ using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.SyncPlay;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -637,6 +638,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<EncodingHelper>();
 
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+
+            serviceCollection.AddSingleton<TranscodingJobHelper>();
         }
 
         /// <summary>
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 39df1e1b13..4d29d38807 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -3,97 +3,277 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Configuration;
 
 namespace Jellyfin.Api.Controllers
 {
-
     /// <summary>
     /// The audio controller.
     /// </summary>
+    // TODO: In order to autheneticate this in the future, Dlna playback will require updating
     public class AudioController : BaseJellyfinApiController
     {
         private readonly IDlnaManager _dlnaManager;
-        private readonly ILogger _logger;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IStreamHelper _streamHelper;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+
+        private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioController"/> class.
         /// </summary>
         /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
-        /// <param name="logger">Instance of the <see cref="ILogger{AuidoController}"/> interface.</param>
-        public AudioController(IDlnaManager dlnaManager, ILogger<AudioController> logger)
+        /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+        public AudioController(
+            IDlnaManager dlnaManager,
+            IUserManager userManger,
+            IAuthorizationContext authorizationContext,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IStreamHelper streamHelper,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper)
         {
             _dlnaManager = dlnaManager;
-            _logger = logger;
+            _authContext = authorizationContext;
+            _userManager = userManger;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _streamHelper = streamHelper;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
-        [HttpGet("{id}/stream.{container}")]
-        [HttpGet("{id}/stream")]
-        [HttpHead("{id}/stream.{container}")]
-        [HttpGet("{id}/stream")]
+        /// <summary>
+        /// Gets an audio stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The audio container.</param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("{itemId}/stream.{container}")]
+        [HttpGet("{itemId}/stream")]
+        [HttpHead("{itemId}/stream.{container}")]
+        [HttpGet("{itemId}/stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetAudioStream(
-            [FromRoute] string id,
-            [FromRoute] string container,
-            [FromQuery] bool Static,
-            [FromQuery] string tag)
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
         {
             bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
 
             var cancellationTokenSource = new CancellationTokenSource();
 
-            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
+            var state = await StreamingHelpers.GetStreamingState(
+                    itemId,
+                    startTimeTicks,
+                    audioCodec,
+                    subtitleCodec,
+                    videoCodec,
+                    @params,
+                    @static,
+                    container,
+                    liveStreamId,
+                    playSessionId,
+                    mediaSourceId,
+                    deviceId,
+                    deviceProfileId,
+                    audioBitRate,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    false,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
 
-            if (Static && state.DirectStreamProvider != null)
+            if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
             {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
                 using (state)
                 {
-                    var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-                    // TODO: Don't hardcode this
-                    outputHeaders[HeaderNames.ContentType] = MimeTypes.GetMimeType("file.ts");
+                    // TODO AllowEndOfFile = false
+                    await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
 
-                    return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, _logger, CancellationToken.None)
-                    {
-                        AllowEndOfFile = false
-                    };
+                    // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+                    return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
                 }
             }
 
             // Static remote stream
-            if (Static && state.InputProtocol == MediaProtocol.Http)
+            if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
             {
-                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
                 using (state)
                 {
-                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false);
                 }
             }
 
-            if (Static && state.InputProtocol != MediaProtocol.File)
+            if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
             {
-                throw new ArgumentException(string.Format($"Input protocol {state.InputProtocol} cannot be streamed statically."));
+                return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
             }
 
             var outputPath = state.OutputFilePath;
-            var outputPathExists = File.Exists(outputPath);
+            var outputPathExists = System.IO.File.Exists(outputPath);
 
-            var transcodingJob = TranscodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
             var isTranscodeCached = outputPathExists && transcodingJob != null;
 
-            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, Static || isTranscodeCached, Request, _dlnaManager);
+            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
 
             // Static stream
-            if (Static)
+            if (@static.HasValue && @static.Value)
             {
                 var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
 
@@ -101,16 +281,10 @@ namespace Jellyfin.Api.Controllers
                 {
                     if (state.MediaSource.IsInfiniteStream)
                     {
-                        var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-                        {
-                            [HeaderNames.ContentType] = contentType
-                        };
-
+                        // TODO AllowEndOfFile = false
+                        await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
 
-                        return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, _logger, CancellationToken.None)
-                        {
-                            AllowEndOfFile = false
-                        };
+                        return File(Response.Body, contentType);
                     }
 
                     TimeSpan? cacheDuration = null;
@@ -120,57 +294,65 @@ namespace Jellyfin.Api.Controllers
                         cacheDuration = TimeSpan.FromDays(365);
                     }
 
+                    return FileStreamResponseHelpers.GetStaticFileResult(
+                        state.MediaPath,
+                        contentType,
+                        _fileSystem.GetLastWriteTimeUtc(state.MediaPath),
+                        cacheDuration,
+                        isHeadRequest,
+                        this);
+                }
+            }
+
+            /*
+            // Not static but transcode cache file exists
+            if (isTranscodeCached && state.VideoRequest == null)
+            {
+                var contentType = state.GetMimeType(outputPath)
+                try
+                {
+                    if (transcodingJob != null)
+                    {
+                        ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
+                    }
                     return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
                     {
                         ResponseHeaders = responseHeaders,
                         ContentType = contentType,
                         IsHeadRequest = isHeadRequest,
-                        Path = state.MediaPath,
-                        CacheDuration = cacheDuration
-
+                        Path = outputPath,
+                        FileShare = FileShare.ReadWrite,
+                        OnComplete = () =>
+                        {
+                            if (transcodingJob != null)
+                            {
+                                ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
+                            }
                     }).ConfigureAwait(false);
                 }
+                finally
+                {
+                    state.Dispose();
+                }
             }
+            */
 
-            //// Not static but transcode cache file exists
-            //if (isTranscodeCached && state.VideoRequest == null)
-            //{
-            //    var contentType = state.GetMimeType(outputPath);
-
-            //    try
-            //    {
-            //        if (transcodingJob != null)
-            //        {
-            //            ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
-            //        }
-
-            //        return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            //        {
-            //            ResponseHeaders = responseHeaders,
-            //            ContentType = contentType,
-            //            IsHeadRequest = isHeadRequest,
-            //            Path = outputPath,
-            //            FileShare = FileShare.ReadWrite,
-            //            OnComplete = () =>
-            //            {
-            //                if (transcodingJob != null)
-            //                {
-            //                    ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
-            //                }
-            //            }
-
-            //        }).ConfigureAwait(false);
-            //    }
-            //    finally
-            //    {
-            //        state.Dispose();
-            //    }
-            //}
-
-            // Need to start ffmpeg
+            // Need to start ffmpeg (because media can't be returned directly)
             try
             {
-                return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+                var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+                var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+                var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+                return await FileStreamResponseHelpers.GetTranscodedFile(
+                    state,
+                    isHeadRequest,
+                    _streamHelper,
+                    this,
+                    _transcodingJobHelper,
+                    ffmpegCommandLineArguments,
+                    Request,
+                    _transcodingJobType,
+                    cancellationTokenSource).ConfigureAwait(false);
             }
             catch
             {
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 05a6edf4ed..da69ca72c9 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -40,8 +39,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
-        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
         public PlaystateController(
             IUserManager userManager,
             IUserDataManager userDataRepository,
@@ -49,8 +47,7 @@ namespace Jellyfin.Api.Controllers
             ISessionManager sessionManager,
             IAuthorizationContext authContext,
             ILoggerFactory loggerFactory,
-            IMediaSourceManager mediaSourceManager,
-            IFileSystem fileSystem)
+            TranscodingJobHelper transcodingJobHelper)
         {
             _userManager = userManager;
             _userDataRepository = userDataRepository;
@@ -59,10 +56,7 @@ namespace Jellyfin.Api.Controllers
             _authContext = authContext;
             _logger = loggerFactory.CreateLogger<PlaystateController>();
 
-            _transcodingJobHelper = new TranscodingJobHelper(
-                loggerFactory.CreateLogger<TranscodingJobHelper>(),
-                mediaSourceManager,
-                fileSystem);
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
         /// <summary>
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
new file mode 100644
index 0000000000..e03cafe35d
--- /dev/null
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -0,0 +1,236 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// The stream response helpers.
+    /// </summary>
+    public static class FileStreamResponseHelpers
+    {
+        /// <summary>
+        /// Returns a static file from a remote source.
+        /// </summary>
+        /// <param name="state">The current <see cref="StreamState"/>.</param>
+        /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
+        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
+        /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
+        public static async Task<ActionResult> GetStaticRemoteStreamResult(
+            StreamState state,
+            bool isHeadRequest,
+            ControllerBase controller,
+            CancellationTokenSource cancellationTokenSource)
+        {
+            HttpClient httpClient = new HttpClient();
+            var responseHeaders = controller.Response.Headers;
+
+            if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
+            {
+                httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
+            }
+
+            var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
+            var contentType = response.Content.Headers.ContentType.ToString();
+
+            responseHeaders[HeaderNames.AcceptRanges] = "none";
+
+            // Seeing cases of -1 here
+            if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value >= 0)
+            {
+                responseHeaders[HeaderNames.ContentLength] = response.Content.Headers.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
+            }
+
+            if (isHeadRequest)
+            {
+                using (response)
+                {
+                    return controller.File(Array.Empty<byte>(), contentType);
+                }
+            }
+
+            return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
+        }
+
+        /// <summary>
+        /// Returns a static file from the server.
+        /// </summary>
+        /// <param name="path">The path to the file.</param>
+        /// <param name="contentType">The content type of the file.</param>
+        /// <param name="dateLastModified">The <see cref="DateTime"/> of the last modification of the file.</param>
+        /// <param name="cacheDuration">The cache duration of the file.</param>
+        /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
+        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <returns>An <see cref="ActionResult"/> the file.</returns>
+        // TODO: caching doesn't work
+        public static ActionResult GetStaticFileResult(
+            string path,
+            string contentType,
+            DateTime dateLastModified,
+            TimeSpan? cacheDuration,
+            bool isHeadRequest,
+            ControllerBase controller)
+        {
+            bool disableCaching = false;
+            if (controller.Request.Headers.TryGetValue(HeaderNames.CacheControl, out StringValues headerValue))
+            {
+                disableCaching = headerValue.FirstOrDefault().Contains("no-cache", StringComparison.InvariantCulture);
+            }
+
+            bool parsingSuccessful = DateTime.TryParseExact(controller.Request.Headers[HeaderNames.IfModifiedSince], "ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false), DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime ifModifiedSinceHeader);
+
+            // if the parsing of the IfModifiedSince header was not successfull, disable caching
+            if (!parsingSuccessful)
+            {
+                disableCaching = true;
+            }
+
+            controller.Response.ContentType = contentType;
+            controller.Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateLastModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+            controller.Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
+
+            if (disableCaching)
+            {
+                controller.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
+                controller.Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
+            }
+            else
+            {
+                if (cacheDuration.HasValue)
+                {
+                    controller.Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
+                }
+                else
+                {
+                    controller.Response.Headers.Add(HeaderNames.CacheControl, "public");
+                }
+
+                controller.Response.Headers.Add(HeaderNames.LastModified, dateLastModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+
+                // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
+                if (!(dateLastModified > ifModifiedSinceHeader))
+                {
+                    if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+                    {
+                        controller.Response.StatusCode = StatusCodes.Status304NotModified;
+                        return new ContentResult();
+                    }
+                }
+            }
+
+            // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+            if (isHeadRequest)
+            {
+                return controller.NoContent();
+            }
+
+            var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
+            return controller.File(stream, contentType);
+        }
+
+        /// <summary>
+        /// Returns a transcoded file from the server.
+        /// </summary>
+        /// <param name="state">The current <see cref="StreamState"/>.</param>
+        /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+        /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
+        /// <param name="request">The <see cref="HttpRequest"/> starting the transcoding.</param>
+        /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+        /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
+        /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
+        public static async Task<ActionResult> GetTranscodedFile(
+            StreamState state,
+            bool isHeadRequest,
+            IStreamHelper streamHelper,
+            ControllerBase controller,
+            TranscodingJobHelper transcodingJobHelper,
+            string ffmpegCommandLineArguments,
+            HttpRequest request,
+            TranscodingJobType transcodingJobType,
+            CancellationTokenSource cancellationTokenSource)
+        {
+            IHeaderDictionary responseHeaders = controller.Response.Headers;
+            // Use the command line args with a dummy playlist path
+            var outputPath = state.OutputFilePath;
+
+            responseHeaders[HeaderNames.AcceptRanges] = "none";
+
+            var contentType = state.GetMimeType(outputPath);
+
+            // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
+            // TODO (from api-migration): Investigate if this is still neccessary as we migrated away from ServiceStack
+            var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
+
+            if (contentLength.HasValue)
+            {
+                responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
+            }
+            else
+            {
+                responseHeaders.Remove(HeaderNames.ContentLength);
+            }
+
+            // Headers only
+            if (isHeadRequest)
+            {
+                return controller.File(Array.Empty<byte>(), contentType);
+            }
+
+            var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
+            await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            try
+            {
+                if (!File.Exists(outputPath))
+                {
+                    await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
+                }
+                else
+                {
+                    transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                    state.Dispose();
+                }
+
+                Stream stream = new MemoryStream();
+
+                await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false);
+                return controller.File(stream, contentType);
+            }
+            finally
+            {
+                transcodingLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Gets the length of the estimated content.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.Nullable{System.Int64}.</returns>
+        private static long? GetEstimatedContentLength(StreamState state)
+        {
+            var totalBitrate = state.TotalOutputBitrate ?? 0;
+
+            if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
+            {
+                return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
+            }
+
+            return null;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 4cebf40f6d..c88ec0b2f2 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -1,32 +1,255 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
 using System.Linq;
-using Jellyfin.Api.Models;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
 
 namespace Jellyfin.Api.Helpers
 {
     /// <summary>
-    /// The streaming helpers
+    /// The streaming helpers.
     /// </summary>
-    public class StreamingHelpers
+    public static class StreamingHelpers
     {
+        public static async Task<StreamState> GetStreamingState(
+            Guid itemId,
+            long? startTimeTicks,
+            string? audioCodec,
+            string? subtitleCodec,
+            string? videoCodec,
+            string? @params,
+            bool? @static,
+            string? container,
+            string? liveStreamId,
+            string? playSessionId,
+            string? mediaSourceId,
+            string? deviceId,
+            string? deviceProfileId,
+            int? audioBitRate,
+            HttpRequest request,
+            IAuthorizationContext authorizationContext,
+            IMediaSourceManager mediaSourceManager,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDlnaManager dlnaManager,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            TranscodingJobType transcodingJobType,
+            bool isVideoRequest,
+            CancellationToken cancellationToken)
+        {
+            EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+            // Parse the DLNA time seek header
+            if (!startTimeTicks.HasValue)
+            {
+                var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
+
+                startTimeTicks = ParseTimeSeekHeader(timeSeek);
+            }
+
+            if (!string.IsNullOrWhiteSpace(@params))
+            {
+                // What is this?
+                ParseParams(request);
+            }
+
+            var streamOptions = ParseStreamOptions(request.Query);
+
+            var url = request.Path.Value.Split('.').Last();
+
+            if (string.IsNullOrEmpty(audioCodec))
+            {
+                audioCodec = encodingHelper.InferAudioCodec(url);
+            }
+
+            var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) ||
+                                    string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
+
+            var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
+            {
+                // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive
+                Request = request,
+                RequestedUrl = url,
+                UserAgent = request.Headers[HeaderNames.UserAgent],
+                EnableDlnaHeaders = enableDlnaHeaders
+            };
+
+            var auth = authorizationContext.GetAuthorizationInfo(request);
+            if (!auth.UserId.Equals(Guid.Empty))
+            {
+                state.User = userManager.GetUserById(auth.UserId);
+            }
+
+            /*
+            if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
+                (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
+                (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                state.SegmentLength = 6;
+            }
+            */
+
+            if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
+            {
+                state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+            }
+
+            if (!string.IsNullOrWhiteSpace(audioCodec))
+            {
+                state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
+                                           ?? state.SupportedAudioCodecs.FirstOrDefault();
+            }
+
+            if (!string.IsNullOrWhiteSpace(subtitleCodec))
+            {
+                state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+                                              ?? state.SupportedSubtitleCodecs.FirstOrDefault();
+            }
+
+            var item = libraryManager.GetItemById(itemId);
+
+            state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+
+            /*
+            var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
+                         item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
+            if (primaryImage != null)
+            {
+                state.AlbumCoverPath = primaryImage.Path;
+            }
+            */
+
+            MediaSourceInfo? mediaSource = null;
+            if (string.IsNullOrWhiteSpace(liveStreamId))
+            {
+                var currentJob = !string.IsNullOrWhiteSpace(playSessionId)
+                    ? transcodingJobHelper.GetTranscodingJob(playSessionId)
+                    : null;
+
+                if (currentJob != null)
+                {
+                    mediaSource = currentJob.MediaSource;
+                }
+
+                if (mediaSource == null)
+                {
+                    var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false);
+
+                    mediaSource = string.IsNullOrEmpty(mediaSourceId)
+                        ? mediaSources[0]
+                        : mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture));
+
+                    if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId)
+                    {
+                        mediaSource = mediaSources[0];
+                    }
+                }
+            }
+            else
+            {
+                var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false);
+                mediaSource = liveStreamInfo.Item1;
+                state.DirectStreamProvider = liveStreamInfo.Item2;
+            }
+
+            encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
+
+            var containerInternal = Path.GetExtension(state.RequestedUrl);
+
+            if (string.IsNullOrEmpty(container))
+            {
+                containerInternal = container;
+            }
+
+            if (string.IsNullOrEmpty(containerInternal))
+            {
+                containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
+            }
+
+            state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+
+            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream);
+
+            state.OutputAudioCodec = audioCodec;
+
+            state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
+
+            if (isVideoRequest)
+            {
+                state.OutputVideoCodec = state.VideoRequest.VideoCodec;
+                state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
+
+                encodingHelper.TryStreamCopy(state);
+
+                if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+                {
+                    var resolution = ResolutionNormalizer.Normalize(
+                        state.VideoStream?.BitRate,
+                        state.VideoStream?.Width,
+                        state.VideoStream?.Height,
+                        state.OutputVideoBitrate.Value,
+                        state.VideoStream?.Codec,
+                        state.OutputVideoCodec,
+                        videoRequest.MaxWidth,
+                        videoRequest.MaxHeight);
+
+                    videoRequest.MaxWidth = resolution.MaxWidth;
+                    videoRequest.MaxHeight = resolution.MaxHeight;
+                }
+            }
+
+            ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static);
+
+            var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
+                ? GetOutputFileExtension(state)
+                : ('.' + state.OutputContainer);
+
+            state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId);
+
+            return state;
+        }
+
         /// <summary>
         /// Adds the dlna headers.
         /// </summary>
         /// <param name="state">The state.</param>
         /// <param name="responseHeaders">The response headers.</param>
         /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
+        /// <param name="startTimeTicks">The start time in ticks.</param>
         /// <param name="request">The <see cref="HttpRequest"/>.</param>
         /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
         public static void AddDlnaHeaders(
             StreamState state,
             IHeaderDictionary responseHeaders,
             bool isStaticallyStreamed,
+            long? startTimeTicks,
             HttpRequest request,
             IDlnaManager dlnaManager)
         {
@@ -54,7 +277,7 @@ namespace Jellyfin.Api.Helpers
 
                 if (!isStaticallyStreamed && profile != null)
                 {
-                    AddTimeSeekResponseHeaders(state, responseHeaders);
+                    AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
                 }
             }
 
@@ -82,51 +305,18 @@ namespace Jellyfin.Api.Helpers
             {
                 var videoCodec = state.ActualOutputVideoCodec;
 
-                responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildVideoHeader(
-                    state.OutputContainer,
-                    videoCodec,
-                    audioCodec,
-                    state.OutputWidth,
-                    state.OutputHeight,
-                    state.TargetVideoBitDepth,
-                    state.OutputVideoBitrate,
-                    state.TargetTimestamp,
-                    isStaticallyStreamed,
-                    state.RunTimeTicks,
-                    state.TargetVideoProfile,
-                    state.TargetVideoLevel,
-                    state.TargetFramerate,
-                    state.TargetPacketLength,
-                    state.TranscodeSeekInfo,
-                    state.IsTargetAnamorphic,
-                    state.IsTargetInterlaced,
-                    state.TargetRefFrames,
-                    state.TargetVideoStreamCount,
-                    state.TargetAudioStreamCount,
-                    state.TargetVideoCodecTag,
-                    state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
-            }
-        }
-
-        /// <summary>
-        /// Parses the dlna headers.
-        /// </summary>
-        /// <param name="startTimeTicks">The start time ticks.</param>
-        /// <param name="request">The <see cref="HttpRequest"/>.</param>
-        public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request)
-        {
-            if (!startTimeTicks.HasValue)
-            {
-                var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
-
-                startTimeTicks = ParseTimeSeekHeader(timeSeek);
+                responseHeaders.Add(
+                    "contentFeatures.dlna.org",
+                    new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
             }
         }
 
         /// <summary>
         /// Parses the time seek header.
         /// </summary>
-        public long? ParseTimeSeekHeader(string value)
+        /// <param name="value">The time seek header string.</param>
+        /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
+        public static long? ParseTimeSeekHeader(string value)
         {
             if (string.IsNullOrWhiteSpace(value))
             {
@@ -138,12 +328,13 @@ namespace Jellyfin.Api.Helpers
             {
                 throw new ArgumentException("Invalid timeseek header");
             }
-            int index = value.IndexOf('-');
+
+            int index = value.IndexOf('-', StringComparison.InvariantCulture);
             value = index == -1
                 ? value.Substring(Npt.Length)
                 : value.Substring(Npt.Length, index - Npt.Length);
 
-            if (value.IndexOf(':') == -1)
+            if (value.IndexOf(':', StringComparison.InvariantCulture) == -1)
             {
                 // Parses npt times in the format of '417.33'
                 if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
@@ -169,15 +360,45 @@ namespace Jellyfin.Api.Helpers
                 {
                     throw new ArgumentException("Invalid timeseek header");
                 }
+
                 timeFactor /= 60;
             }
+
             return TimeSpan.FromSeconds(secondsSum).Ticks;
         }
 
-        public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders)
+        /// <summary>
+        /// Parses query parameters as StreamOptions.
+        /// </summary>
+        /// <param name="queryString">The query string.</param>
+        /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
+        public static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
         {
-            var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-            var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+            Dictionary<string, string> streamOptions = new Dictionary<string, string>();
+            foreach (var param in queryString)
+            {
+                if (char.IsLower(param.Key[0]))
+                {
+                    // This was probably not parsed initially and should be a StreamOptions
+                    // or the generated URL should correctly serialize it
+                    // TODO: This should be incorporated either in the lower framework for parsing requests
+                    streamOptions[param.Key] = param.Value;
+                }
+            }
+
+            return streamOptions;
+        }
+
+        /// <summary>
+        /// Adds the dlna time seek headers to the response.
+        /// </summary>
+        /// <param name="state">The current <see cref="StreamState"/>.</param>
+        /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
+        /// <param name="startTimeTicks">The start time in ticks.</param>
+        public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
+        {
+            var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
+            var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
 
             responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
                 CultureInfo.InvariantCulture,
@@ -190,5 +411,369 @@ namespace Jellyfin.Api.Helpers
                 startSeconds,
                 runtimeSeconds));
         }
+
+        /// <summary>
+        /// Gets the output file extension.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <returns>System.String.</returns>
+        public static string? GetOutputFileExtension(StreamState state)
+        {
+            var ext = Path.GetExtension(state.RequestedUrl);
+
+            if (!string.IsNullOrEmpty(ext))
+            {
+                return ext;
+            }
+
+            var isVideoRequest = state.VideoRequest != null;
+
+            // Try to infer based on the desired video codec
+            if (isVideoRequest)
+            {
+                var videoCodec = state.VideoRequest.VideoCodec;
+
+                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".ts";
+                }
+
+                if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".ogv";
+                }
+
+                if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".webm";
+                }
+
+                if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".asf";
+                }
+            }
+
+            // Try to infer based on the desired audio codec
+            if (!isVideoRequest)
+            {
+                var audioCodec = state.Request.AudioCodec;
+
+                if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".aac";
+                }
+
+                if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".mp3";
+                }
+
+                if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".ogg";
+                }
+
+                if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ".wma";
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the output file path for transcoding.
+        /// </summary>
+        /// <param name="state">The current <see cref="StreamState"/>.</param>
+        /// <param name="outputFileExtension">The file extension of the output file.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="deviceId">The device id.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
+        private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+        {
+            var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
+
+            var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+            var ext = outputFileExtension?.ToLowerInvariant();
+            var folder = serverConfigurationManager.GetTranscodePath();
+
+            return Path.Combine(folder, filename + ext);
+        }
+
+        private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
+        {
+            var headers = request.Headers;
+
+            if (!string.IsNullOrWhiteSpace(deviceProfileId))
+            {
+                state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
+            }
+            else if (!string.IsNullOrWhiteSpace(deviceProfileId))
+            {
+                var caps = deviceManager.GetCapabilities(deviceProfileId);
+
+                state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
+            }
+
+            var profile = state.DeviceProfile;
+
+            if (profile == null)
+            {
+                // Don't use settings from the default profile.
+                // Only use a specific profile if it was requested.
+                return;
+            }
+
+            var audioCodec = state.ActualOutputAudioCodec;
+            var videoCodec = state.ActualOutputVideoCodec;
+
+            var mediaProfile = state.VideoRequest == null
+                ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
+                : profile.GetVideoMediaProfile(
+                    state.OutputContainer,
+                    audioCodec,
+                    videoCodec,
+                    state.OutputWidth,
+                    state.OutputHeight,
+                    state.TargetVideoBitDepth,
+                    state.OutputVideoBitrate,
+                    state.TargetVideoProfile,
+                    state.TargetVideoLevel,
+                    state.TargetFramerate,
+                    state.TargetPacketLength,
+                    state.TargetTimestamp,
+                    state.IsTargetAnamorphic,
+                    state.IsTargetInterlaced,
+                    state.TargetRefFrames,
+                    state.TargetVideoStreamCount,
+                    state.TargetAudioStreamCount,
+                    state.TargetVideoCodecTag,
+                    state.IsTargetAVC);
+
+            if (mediaProfile != null)
+            {
+                state.MimeType = mediaProfile.MimeType;
+            }
+
+            if (!(@static.HasValue && @static.Value))
+            {
+                var transcodingProfile = state.VideoRequest == null ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+
+                if (transcodingProfile != null)
+                {
+                    state.EstimateContentLength = transcodingProfile.EstimateContentLength;
+                    // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+                    state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+                    if (state.VideoRequest != null)
+                    {
+                        state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
+                        state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Parses the parameters.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        private void ParseParams(StreamRequest request)
+        {
+            var vals = request.Params.Split(';');
+
+            var videoRequest = request as VideoStreamRequest;
+
+            for (var i = 0; i < vals.Length; i++)
+            {
+                var val = vals[i];
+
+                if (string.IsNullOrWhiteSpace(val))
+                {
+                    continue;
+                }
+
+                switch (i)
+                {
+                    case 0:
+                        request.DeviceProfileId = val;
+                        break;
+                    case 1:
+                        request.DeviceId = val;
+                        break;
+                    case 2:
+                        request.MediaSourceId = val;
+                        break;
+                    case 3:
+                        request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        break;
+                    case 4:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.VideoCodec = val;
+                        }
+
+                        break;
+                    case 5:
+                        request.AudioCodec = val;
+                        break;
+                    case 6:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 7:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 8:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 9:
+                        request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
+                        break;
+                    case 10:
+                        request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+                        break;
+                    case 11:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 12:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 13:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 14:
+                        request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
+                        break;
+                    case 15:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.Level = val;
+                        }
+
+                        break;
+                    case 16:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 17:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
+                        }
+
+                        break;
+                    case 18:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.Profile = val;
+                        }
+
+                        break;
+                    case 19:
+                        // cabac no longer used
+                        break;
+                    case 20:
+                        request.PlaySessionId = val;
+                        break;
+                    case 21:
+                        // api_key
+                        break;
+                    case 22:
+                        request.LiveStreamId = val;
+                        break;
+                    case 23:
+                        // Duplicating ItemId because of MediaMonkey
+                        break;
+                    case 24:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        }
+
+                        break;
+                    case 25:
+                        if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
+                        {
+                            if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
+                            {
+                                videoRequest.SubtitleMethod = method;
+                            }
+                        }
+
+                        break;
+                    case 26:
+                        request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
+                        break;
+                    case 27:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        }
+
+                        break;
+                    case 28:
+                        request.Tag = val;
+                        break;
+                    case 29:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        }
+
+                        break;
+                    case 30:
+                        request.SubtitleCodec = val;
+                        break;
+                    case 31:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        }
+
+                        break;
+                    case 32:
+                        if (videoRequest != null)
+                        {
+                            videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                        }
+
+                        break;
+                    case 33:
+                        request.TranscodeReasons = val;
+                        break;
+                }
+            }
+        }
     }
 }
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 7db75387a1..9fbd5ec2db 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -1,16 +1,28 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Text;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Api.Models;
 using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Helpers
@@ -30,9 +42,17 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
 
+        private readonly IAuthorizationContext _authorizationContext;
+        private readonly EncodingHelper _encodingHelper;
+        private readonly IFileSystem _fileSystem;
+        private readonly IIsoManager _isoManager;
+
         private readonly ILogger<TranscodingJobHelper> _logger;
+        private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly ISessionManager _sessionManager;
+        private readonly ILoggerFactory _loggerFactory;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
@@ -40,14 +60,40 @@ namespace Jellyfin.Api.Helpers
         /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="isoManager">Instance of the <see cref="IIsoManager"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         public TranscodingJobHelper(
             ILogger<TranscodingJobHelper> logger,
             IMediaSourceManager mediaSourceManager,
-            IFileSystem fileSystem)
+            IFileSystem fileSystem,
+            IMediaEncoder mediaEncoder,
+            IServerConfigurationManager serverConfigurationManager,
+            ISessionManager sessionManager,
+            IAuthorizationContext authorizationContext,
+            IIsoManager isoManager,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            ILoggerFactory loggerFactory)
         {
             _logger = logger;
             _mediaSourceManager = mediaSourceManager;
             _fileSystem = fileSystem;
+            _mediaEncoder = mediaEncoder;
+            _serverConfigurationManager = serverConfigurationManager;
+            _sessionManager = sessionManager;
+            _authorizationContext = authorizationContext;
+            _isoManager = isoManager;
+            _loggerFactory = loggerFactory;
+
+            _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
+            DeleteEncodedMediaCache();
         }
 
         /// <summary>
@@ -63,7 +109,13 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
-        public static TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+        /// <summary>
+        /// Get transcoding job.
+        /// </summary>
+        /// <param name="path">Path to the transcoding file.</param>
+        /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
+        /// <returns>The transcoding job.</returns>
+        public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
         {
             lock (_activeTranscodingJobs)
             {
@@ -361,14 +413,24 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        /// <summary>
+        /// Report the transcoding progress to the session manager.
+        /// </summary>
+        /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
+        /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
+        /// <param name="transcodingPosition">The current transcoding position.</param>
+        /// <param name="framerate">The framerate of the transcoding job.</param>
+        /// <param name="percentComplete">The completion percentage of the transcode.</param>
+        /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
+        /// <param name="bitRate">The bitrate of the transcoding job.</param>
         public void ReportTranscodingProgress(
-        TranscodingJob job,
-        StreamState state,
-        TimeSpan? transcodingPosition,
-        float? framerate,
-        double? percentComplete,
-        long? bytesTranscoded,
-        int? bitRate)
+            TranscodingJobDto job,
+            StreamState state,
+            TimeSpan? transcodingPosition,
+            float? framerate,
+            double? percentComplete,
+            long? bytesTranscoded,
+            int? bitRate)
         {
             var ticks = transcodingPosition?.Ticks;
 
@@ -405,5 +467,374 @@ namespace Jellyfin.Api.Helpers
                 });
             }
         }
+
+        /// <summary>
+        /// Starts the FFMPEG.
+        /// </summary>
+        /// <param name="state">The state.</param>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
+        /// <param name="request">The <see cref="HttpRequest"/>.</param>
+        /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+        /// <param name="cancellationTokenSource">The cancellation token source.</param>
+        /// <param name="workingDirectory">The working directory.</param>
+        /// <returns>Task.</returns>
+        public async Task<TranscodingJobDto> StartFfMpeg(
+            StreamState state,
+            string outputPath,
+            string commandLineArguments,
+            HttpRequest request,
+            TranscodingJobType transcodingJobType,
+            CancellationTokenSource cancellationTokenSource,
+            string workingDirectory = null)
+        {
+            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+
+            await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
+
+            if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            {
+                var auth = _authorizationContext.GetAuthorizationInfo(request);
+                if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
+                {
+                    this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+
+                    throw new ArgumentException("User does not have access to video transcoding");
+                }
+            }
+
+            var process = new Process()
+            {
+                StartInfo = new ProcessStartInfo()
+                {
+                    WindowStyle = ProcessWindowStyle.Hidden,
+                    CreateNoWindow = true,
+                    UseShellExecute = false,
+
+                    // Must consume both stdout and stderr or deadlocks may occur
+                    // RedirectStandardOutput = true,
+                    RedirectStandardError = true,
+                    RedirectStandardInput = true,
+                    FileName = _mediaEncoder.EncoderPath,
+                    Arguments = commandLineArguments,
+                    WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
+                    ErrorDialog = false
+                },
+                EnableRaisingEvents = true
+            };
+
+            var transcodingJob = this.OnTranscodeBeginning(
+                outputPath,
+                state.Request.PlaySessionId,
+                state.MediaSource.LiveStreamId,
+                Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
+                transcodingJobType,
+                process,
+                state.Request.DeviceId,
+                state,
+                cancellationTokenSource);
+
+            var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+            _logger.LogInformation(commandLineLogMessage);
+
+            var logFilePrefix = "ffmpeg-transcode";
+            if (state.VideoRequest != null
+                && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            {
+                logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
+                    ? "ffmpeg-remux"
+                    : "ffmpeg-directstream";
+            }
+
+            var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
+
+            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+            Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+
+            var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+            await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
+
+            process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
+
+            try
+            {
+                process.Start();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error starting ffmpeg");
+
+                this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+
+                throw;
+            }
+
+            _logger.LogDebug("Launched ffmpeg process");
+            state.TranscodingJob = transcodingJob;
+
+            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+            _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
+
+            // Wait for the file to exist before proceeeding
+            var ffmpegTargetFile = state.WaitForPath ?? outputPath;
+            _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
+            while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
+            {
+                await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
+            }
+
+            _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
+
+            if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
+            {
+                await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
+
+                if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
+                {
+                    await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
+                }
+            }
+
+            if (!transcodingJob.HasExited)
+            {
+                StartThrottler(state, transcodingJob);
+            }
+
+            _logger.LogDebug("StartFfMpeg() finished successfully");
+
+            return transcodingJob;
+        }
+
+        private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
+        {
+            if (EnableThrottling(state))
+            {
+                transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
+                state.TranscodingThrottler.Start();
+            }
+        }
+
+        private bool EnableThrottling(StreamState state)
+        {
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+
+            // enable throttling when NOT using hardware acceleration
+            if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+            {
+                return state.InputProtocol == MediaProtocol.File &&
+                       state.RunTimeTicks.HasValue &&
+                       state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
+                       state.IsInputVideo &&
+                       state.VideoType == VideoType.VideoFile &&
+                       !EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Called when [transcode beginning].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="playSessionId">The play session identifier.</param>
+        /// <param name="liveStreamId">The live stream identifier.</param>
+        /// <param name="transcodingJobId">The transcoding job identifier.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="process">The process.</param>
+        /// <param name="deviceId">The device id.</param>
+        /// <param name="state">The state.</param>
+        /// <param name="cancellationTokenSource">The cancellation token source.</param>
+        /// <returns>TranscodingJob.</returns>
+        public TranscodingJobDto OnTranscodeBeginning(
+            string path,
+            string playSessionId,
+            string liveStreamId,
+            string transcodingJobId,
+            TranscodingJobType type,
+            Process process,
+            string deviceId,
+            StreamState state,
+            CancellationTokenSource cancellationTokenSource)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
+                {
+                    Type = type,
+                    Path = path,
+                    Process = process,
+                    ActiveRequestCount = 1,
+                    DeviceId = deviceId,
+                    CancellationTokenSource = cancellationTokenSource,
+                    Id = transcodingJobId,
+                    PlaySessionId = playSessionId,
+                    LiveStreamId = liveStreamId,
+                    MediaSource = state.MediaSource
+                };
+
+                _activeTranscodingJobs.Add(job);
+
+                ReportTranscodingProgress(job, state, null, null, null, null, null);
+
+                return job;
+            }
+        }
+
+        /// <summary>
+        /// <summary>
+        /// The progressive
+        /// </summary>
+        /// Called when [transcode failed to start].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="state">The state.</param>
+        public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+
+                if (job != null)
+                {
+                    _activeTranscodingJobs.Remove(job);
+                }
+            }
+
+            lock (_transcodingLocks)
+            {
+                _transcodingLocks.Remove(path);
+            }
+
+            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
+            {
+                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
+            }
+        }
+
+        /// <summary>
+        /// Processes the exited.
+        /// </summary>
+        /// <param name="process">The process.</param>
+        /// <param name="job">The job.</param>
+        /// <param name="state">The state.</param>
+        private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
+        {
+            if (job != null)
+            {
+                job.HasExited = true;
+            }
+
+            _logger.LogDebug("Disposing stream resources");
+            state.Dispose();
+
+            if (process.ExitCode == 0)
+            {
+                _logger.LogInformation("FFMpeg exited with code 0");
+            }
+            else
+            {
+                _logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
+            }
+
+            process.Dispose();
+        }
+
+        private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
+        {
+            if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath))
+            {
+                state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
+            }
+
+            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
+            {
+                var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
+                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+                    cancellationTokenSource.Token)
+                    .ConfigureAwait(false);
+
+                _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
+
+                if (state.VideoRequest != null)
+                {
+                    _encodingHelper.TryStreamCopy(state);
+                }
+            }
+
+            if (state.MediaSource.BufferMs.HasValue)
+            {
+                await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode begin request].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
+        public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+
+                if (job == null)
+                {
+                    return null;
+                }
+
+                OnTranscodeBeginRequest(job);
+
+                return job;
+            }
+        }
+
+        private void OnTranscodeBeginRequest(TranscodingJobDto job)
+        {
+            job.ActiveRequestCount++;
+
+            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
+            {
+                job.StopKillTimer();
+            }
+        }
+
+        /// <summary>
+        /// Gets the transcoding lock.
+        /// </summary>
+        /// <param name="outputPath">The output path of the transcoded file.</param>
+        /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
+        public SemaphoreSlim GetTranscodingLock(string outputPath)
+        {
+            lock (_transcodingLocks)
+            {
+                if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
+                {
+                    result = new SemaphoreSlim(1, 1);
+                    _transcodingLocks[outputPath] = result;
+                }
+
+                return result;
+            }
+        }
+
+        /// <summary>
+        /// Deletes the encoded media cache.
+        /// </summary>
+        private void DeleteEncodedMediaCache()
+        {
+            var path = _serverConfigurationManager.GetTranscodePath();
+            if (!Directory.Exists(path))
+            {
+                return;
+            }
+
+            foreach (var file in _fileSystem.GetFilePaths(path, true))
+            {
+                _fileSystem.DeleteFile(file);
+            }
+        }
     }
 }
diff --git a/Jellyfin.Api/Models/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
similarity index 51%
rename from Jellyfin.Api/Models/StreamState.cs
rename to Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index 9fe5f52c3e..b962e0ac79 100644
--- a/Jellyfin.Api/Models/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -5,36 +5,77 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 
-namespace Jellyfin.Api.Models
+namespace Jellyfin.Api.Models.StreamingDtos
 {
+    /// <summary>
+    /// The stream state dto.
+    /// </summary>
     public class StreamState : EncodingJobInfo, IDisposable
     {
         private readonly IMediaSourceManager _mediaSourceManager;
-        private bool _disposed = false;
-
-        public string RequestedUrl { get; set; }
-
-        public StreamRequest Request
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private bool _disposed;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamState" /> class.
+        /// </summary>
+        /// <param name="mediaSourceManager">Instance of the <see cref="mediaSourceManager" /> interface.</param>
+        /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
+        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
+        public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
+            : base(transcodingType)
         {
-            get => (StreamRequest)BaseRequest;
-            set
-            {
-                BaseRequest = value;
-
-                IsVideoRequest = VideoRequest != null;
-            }
+            _mediaSourceManager = mediaSourceManager;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
-        public TranscodingThrottler TranscodingThrottler { get; set; }
-
+        /// <summary>
+        /// Gets or sets the requested url.
+        /// </summary>
+        public string? RequestedUrl { get; set; }
+
+        // /// <summary>
+        // /// Gets or sets the request.
+        // /// </summary>
+        // public StreamRequest Request
+        // {
+        //     get => (StreamRequest)BaseRequest;
+        //     set
+        //     {
+        //         BaseRequest = value;
+        //
+        //         IsVideoRequest = VideoRequest != null;
+        //     }
+        // }
+
+        /// <summary>
+        /// Gets or sets the transcoding throttler.
+        /// </summary>
+        public TranscodingThrottler? TranscodingThrottler { get; set; }
+
+        /// <summary>
+        /// Gets the video request.
+        /// </summary>
         public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
 
-        public IDirectStreamProvider DirectStreamProvider { get; set; }
+        /// <summary>
+        /// Gets or sets the direct stream provicer.
+        /// </summary>
+        public IDirectStreamProvider? DirectStreamProvider { get; set; }
 
-        public string WaitForPath { get; set; }
+        /// <summary>
+        /// Gets or sets the path to wait for.
+        /// </summary>
+        public string? WaitForPath { get; set; }
 
+        /// <summary>
+        /// Gets a value indicating whether the request outputs video.
+        /// </summary>
         public bool IsOutputVideo => Request is VideoStreamRequest;
 
+        /// <summary>
+        /// Gets the segment length.
+        /// </summary>
         public int SegmentLength
         {
             get
@@ -74,6 +115,9 @@ namespace Jellyfin.Api.Models
             }
         }
 
+        /// <summary>
+        /// Gets the minimum number of segments.
+        /// </summary>
         public int MinSegments
         {
             get
@@ -87,35 +131,53 @@ namespace Jellyfin.Api.Models
             }
         }
 
-        public string UserAgent { get; set; }
+        /// <summary>
+        /// Gets or sets the user agent.
+        /// </summary>
+        public string? UserAgent { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to estimate the content length.
+        /// </summary>
         public bool EstimateContentLength { get; set; }
 
+        /// <summary>
+        /// Gets or sets the transcode seek info.
+        /// </summary>
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to enable dlna headers.
+        /// </summary>
         public bool EnableDlnaHeaders { get; set; }
 
-        public DeviceProfile DeviceProfile { get; set; }
+        /// <summary>
+        /// Gets or sets the device profile.
+        /// </summary>
+        public DeviceProfile? DeviceProfile { get; set; }
 
-        public TranscodingJobDto TranscodingJob { get; set; }
+        /// <summary>
+        /// Gets or sets the transcoding job.
+        /// </summary>
+        public TranscodingJobDto? TranscodingJob { get; set; }
 
-        public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
-            : base(transcodingType)
+        /// <inheritdoc />
+        public void Dispose()
         {
-            _mediaSourceManager = mediaSourceManager;
+            Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
+        /// <inheritdoc />
         public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
         {
-            TranscodingJobHelper.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
-        }
-
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
+            _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
         }
 
+        /// <summary>
+        /// Disposes the stream state.
+        /// </summary>
+        /// <param name="disposing">Whether the object is currently beeing disposed.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
index 34c7986ca5..ef639851bd 100644
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
@@ -17,10 +17,6 @@ namespace MediaBrowser.Api.Playback.Progressive
     /// <summary>
     /// Class GetAudioStream
     /// </summary>
-    [Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
     public class GetAudioStream : StreamRequest
     {
     }
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 8ce106469e..8cfe562b38 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1263,6 +1263,17 @@ namespace MediaBrowser.Controller.MediaEncoding
             return null;
         }
 
+        public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
+        {
+            if (audioBitRate.HasValue)
+            {
+                // Don't encode any higher than this
+                return Math.Min(384000, audioBitRate.Value);
+            }
+
+            return null;
+        }
+
         public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
         {
             var channels = state.OutputAudioChannels;

From 5c66f9e4716961dc40e0444c7d261dfd2e5841d7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 20 Jul 2020 14:43:54 -0600
Subject: [PATCH 320/463] changes from merge

---
 .../Auth/DownloadPolicy/DownloadHandler.cs    |    1 -
 .../Controllers/ActivityLogController.cs      |   10 +-
 .../Controllers/DashboardController.cs        |    1 -
 Jellyfin.Api/Controllers/FilterController.cs  |    1 -
 .../Controllers/ItemRefreshController.cs      |    1 -
 Jellyfin.Api/Controllers/LibraryController.cs |    1 -
 .../Controllers/LibraryStructureController.cs |    1 -
 Jellyfin.Api/Controllers/MoviesController.cs  |    1 -
 .../Controllers/NotificationsController.cs    |    1 -
 Jellyfin.Api/Controllers/PackageController.cs |   38 +-
 Jellyfin.Api/Controllers/PluginsController.cs |    1 -
 Jellyfin.Api/Controllers/TvShowsController.cs |    1 -
 Jellyfin.Api/Controllers/UserController.cs    |    9 +-
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |    1 -
 .../Models/PlaybackDtos/TranscodingJobDto.cs  |    3 -
 MediaBrowser.Api/ChannelService.cs            |  340 -----
 MediaBrowser.Api/ConfigurationService.cs      |  144 --
 MediaBrowser.Api/Devices/DeviceService.cs     |  103 --
 MediaBrowser.Api/DisplayPreferencesService.cs |  101 --
 MediaBrowser.Api/EnvironmentService.cs        |  285 ----
 MediaBrowser.Api/FilterService.cs             |  248 ----
 MediaBrowser.Api/Images/ImageByNameService.cs |  277 ----
 MediaBrowser.Api/Images/RemoteImageService.cs |  296 ----
 MediaBrowser.Api/ItemLookupService.cs         |  336 -----
 MediaBrowser.Api/Library/LibraryService.cs    | 1124 --------------
 .../Library/LibraryStructureService.cs        |  412 ------
 MediaBrowser.Api/LiveTv/LiveTvService.cs      | 1287 -----------------
 MediaBrowser.Api/LocalizationService.cs       |  111 --
 MediaBrowser.Api/Movies/CollectionService.cs  |  104 --
 MediaBrowser.Api/Movies/MoviesService.cs      |  414 ------
 MediaBrowser.Api/Movies/TrailersService.cs    |   88 --
 MediaBrowser.Api/Music/AlbumsService.cs       |  132 --
 MediaBrowser.Api/Music/InstantMixService.cs   |  196 ---
 MediaBrowser.Api/PackageService.cs            |  197 ---
 MediaBrowser.Api/PlaylistService.cs           |  216 ---
 MediaBrowser.Api/PluginService.cs             |  277 ----
 .../ScheduledTasks/ScheduledTaskService.cs    |  234 ---
 MediaBrowser.Api/SearchService.cs             |  332 -----
 MediaBrowser.Api/Sessions/SessionService.cs   |  499 -------
 MediaBrowser.Api/Subtitles/SubtitleService.cs |  302 ----
 MediaBrowser.Api/SuggestionsService.cs        |  103 --
 MediaBrowser.Api/System/ActivityLogService.cs |   69 -
 MediaBrowser.Api/System/SystemService.cs      |  221 ---
 MediaBrowser.Api/TvShowsService.cs            |  497 -------
 .../UserLibrary/ArtistsService.cs             |  143 --
 .../UserLibrary/BaseItemsByNameService.cs     |  388 -----
 .../UserLibrary/BaseItemsRequest.cs           |  478 ------
 MediaBrowser.Api/UserLibrary/GenresService.cs |  140 --
 MediaBrowser.Api/UserLibrary/ItemsService.cs  |  514 -------
 .../UserLibrary/PersonsService.cs             |  146 --
 .../UserLibrary/PlaystateService.cs           |  456 ------
 .../UserLibrary/StudiosService.cs             |  132 --
 .../UserLibrary/UserLibraryService.cs         |  575 --------
 .../UserLibrary/UserViewsService.cs           |  148 --
 MediaBrowser.Api/UserLibrary/YearsService.cs  |  131 --
 MediaBrowser.Api/UserService.cs               |  598 --------
 56 files changed, 43 insertions(+), 12822 deletions(-)
 delete mode 100644 MediaBrowser.Api/ChannelService.cs
 delete mode 100644 MediaBrowser.Api/ConfigurationService.cs
 delete mode 100644 MediaBrowser.Api/Devices/DeviceService.cs
 delete mode 100644 MediaBrowser.Api/DisplayPreferencesService.cs
 delete mode 100644 MediaBrowser.Api/EnvironmentService.cs
 delete mode 100644 MediaBrowser.Api/FilterService.cs
 delete mode 100644 MediaBrowser.Api/Images/ImageByNameService.cs
 delete mode 100644 MediaBrowser.Api/Images/RemoteImageService.cs
 delete mode 100644 MediaBrowser.Api/ItemLookupService.cs
 delete mode 100644 MediaBrowser.Api/Library/LibraryService.cs
 delete mode 100644 MediaBrowser.Api/Library/LibraryStructureService.cs
 delete mode 100644 MediaBrowser.Api/LiveTv/LiveTvService.cs
 delete mode 100644 MediaBrowser.Api/LocalizationService.cs
 delete mode 100644 MediaBrowser.Api/Movies/CollectionService.cs
 delete mode 100644 MediaBrowser.Api/Movies/MoviesService.cs
 delete mode 100644 MediaBrowser.Api/Movies/TrailersService.cs
 delete mode 100644 MediaBrowser.Api/Music/AlbumsService.cs
 delete mode 100644 MediaBrowser.Api/Music/InstantMixService.cs
 delete mode 100644 MediaBrowser.Api/PackageService.cs
 delete mode 100644 MediaBrowser.Api/PlaylistService.cs
 delete mode 100644 MediaBrowser.Api/PluginService.cs
 delete mode 100644 MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
 delete mode 100644 MediaBrowser.Api/SearchService.cs
 delete mode 100644 MediaBrowser.Api/Sessions/SessionService.cs
 delete mode 100644 MediaBrowser.Api/Subtitles/SubtitleService.cs
 delete mode 100644 MediaBrowser.Api/SuggestionsService.cs
 delete mode 100644 MediaBrowser.Api/System/ActivityLogService.cs
 delete mode 100644 MediaBrowser.Api/System/SystemService.cs
 delete mode 100644 MediaBrowser.Api/TvShowsService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/ArtistsService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/GenresService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/ItemsService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/PersonsService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/PlaystateService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/StudiosService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/UserLibraryService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/UserViewsService.cs
 delete mode 100644 MediaBrowser.Api/UserLibrary/YearsService.cs
 delete mode 100644 MediaBrowser.Api/UserService.cs

diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
index fcfa55dfec..b61680ab1a 100644
--- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
+++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
@@ -1,5 +1,4 @@
 using System.Threading.Tasks;
-using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index c287d1a773..12ea249731 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,5 +1,4 @@
 using System;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
@@ -35,6 +34,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
+        /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
         /// <response code="200">Activity log returned.</response>
         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
         [HttpGet("Entries")]
@@ -42,10 +42,14 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] DateTime? minDate)
+            [FromQuery] DateTime? minDate,
+            [FromQuery] bool? hasUserId)
         {
             var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
-                entries => entries.Where(entry => entry.DateCreated >= minDate));
+                entries => entries.Where(entry => entry.DateCreated >= minDate
+                                                  && (!hasUserId.HasValue || (hasUserId.Value
+                                                      ? entry.UserId != Guid.Empty
+                                                      : entry.UserId == Guid.Empty))));
 
             return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
         }
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 699ef6bf7b..e033c50d5c 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using Jellyfin.Api.Models;
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 288d4c5459..9ba5e11618 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,5 +1,4 @@
 using System;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Dto;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 3801ce5b75..4697d869dc 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 2466b2ac89..5ad466c557 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Linq;
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 881d3f1923..d3537a7a70 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Linq;
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 144a7b554f..a9af1a2c33 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 02aa39b248..1bb39b5f76 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Models.NotificationDtos;
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 68ae05658e..a6c552790e 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Updates;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -20,14 +21,17 @@ namespace Jellyfin.Api.Controllers
     public class PackageController : BaseJellyfinApiController
     {
         private readonly IInstallationManager _installationManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PackageController"/> class.
         /// </summary>
-        /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>Installation Manager.</param>
-        public PackageController(IInstallationManager installationManager)
+        /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
         {
             _installationManager = installationManager;
+            _serverConfigurationManager = serverConfigurationManager;
         }
 
         /// <summary>
@@ -110,11 +114,39 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("/Installing/{packageId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public IActionResult CancelPackageInstallation(
+        public ActionResult CancelPackageInstallation(
             [FromRoute] [Required] Guid packageId)
         {
             _installationManager.CancelInstallation(packageId);
             return NoContent();
         }
+
+        /// <summary>
+        /// Gets all package repositories.
+        /// </summary>
+        /// <response code="200">Package repositories returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
+        [HttpGet("/Repositories")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
+        {
+            return _serverConfigurationManager.Configuration.PluginRepositories;
+        }
+
+        /// <summary>
+        /// Sets the enabled and existing package repositories.
+        /// </summary>
+        /// <param name="repositoryInfos">The list of package repositories.</param>
+        /// <response code="204">Package repositories saved.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpOptions("/Repositories")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
+        {
+            _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+            return NoContent();
+        }
     }
 }
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 056395a51d..48bb867aca 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index e5b0436214..d54bc10c03 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 24194dcc23..8038ca0445 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -137,14 +136,8 @@ namespace Jellyfin.Api.Controllers
         public ActionResult DeleteUser([FromRoute] Guid userId)
         {
             var user = _userManager.GetUserById(userId);
-
-            if (user == null)
-            {
-                return NotFound("User not found");
-            }
-
             _sessionManager.RevokeUserTokens(user.Id, null);
-            _userManager.DeleteUser(user);
+            _userManager.DeleteUser(userId);
             return NoContent();
         }
 
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 44f662e6e0..34b371d3f8 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading;
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index dcc3224704..b9507a4e50 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -1,10 +1,7 @@
 using System;
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using System.Threading;
-using System.Threading.Tasks;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dto;
 using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs
deleted file mode 100644
index 8c336b1c9d..0000000000
--- a/MediaBrowser.Api/ChannelService.cs
+++ /dev/null
@@ -1,340 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Channels;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Channels", "GET", Summary = "Gets available channels")]
-    public class GetChannels : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SupportsLatestItems", Description = "Optional. Filter by channels that support getting latest items.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? SupportsLatestItems { get; set; }
-
-        public bool? SupportsMediaDeletion { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is favorite.
-        /// </summary>
-        /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
-        public bool? IsFavorite { get; set; }
-    }
-
-    [Route("/Channels/{Id}/Features", "GET", Summary = "Gets features for a channel")]
-    public class GetChannelFeatures : IReturn<ChannelFeatures>
-    {
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Channels/Features", "GET", Summary = "Gets features for a channel")]
-    public class GetAllChannelFeatures : IReturn<ChannelFeatures[]>
-    {
-    }
-
-    [Route("/Channels/{Id}/Items", "GET", Summary = "Gets channel items")]
-    public class GetChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
-    {
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "FolderId", Description = "Folder Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string FolderId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public IEnumerable<ItemFilter> GetFilters()
-        {
-            var val = Filters;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ItemFilter>()
-                : val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
-        }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public ValueTuple<string, SortOrder>[] GetOrderBy()
-        {
-            return BaseItemsRequest.GetOrderBy(SortBy, SortOrder);
-        }
-    }
-
-    [Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")]
-    public class GetLatestChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "ChannelIds", Description = "Optional. Specify one or more channel id's, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ChannelIds { get; set; }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public IEnumerable<ItemFilter> GetFilters()
-        {
-            return string.IsNullOrEmpty(Filters)
-                ? Array.Empty<ItemFilter>()
-                : Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
-        }
-    }
-
-    [Authenticated]
-    public class ChannelService : BaseApiService
-    {
-        private readonly IChannelManager _channelManager;
-        private IUserManager _userManager;
-
-        public ChannelService(
-            ILogger<ChannelService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IChannelManager channelManager,
-            IUserManager userManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _channelManager = channelManager;
-            _userManager = userManager;
-        }
-
-        public object Get(GetAllChannelFeatures request)
-        {
-            var result = _channelManager.GetAllChannelFeatures();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetChannelFeatures request)
-        {
-            var result = _channelManager.GetChannelFeatures(request.Id);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetChannels request)
-        {
-            var result = _channelManager.GetChannels(new ChannelQuery
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                SupportsLatestItems = request.SupportsLatestItems,
-                SupportsMediaDeletion = request.SupportsMediaDeletion,
-                IsFavorite = request.IsFavorite
-            });
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetChannelItems request)
-        {
-            var user = request.UserId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                ChannelIds = new[] { new Guid(request.Id) },
-                ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId),
-                OrderBy = request.GetOrderBy(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = request.GetItemFields()
-                }
-            };
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetLatestChannelItems request)
-        {
-            var user = request.UserId.Equals(Guid.Empty)
-                ? null
-                : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                ChannelIds = (request.ChannelIds ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToArray(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = request.GetItemFields()
-                }
-            };
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs
deleted file mode 100644
index 19369cccae..0000000000
--- a/MediaBrowser.Api/ConfigurationService.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetConfiguration.
-    /// </summary>
-    [Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
-    [Authenticated]
-    public class GetConfiguration : IReturn<ServerConfiguration>
-    {
-    }
-
-    [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetNamedConfiguration
-    {
-        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Key { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateConfiguration.
-    /// </summary>
-    [Route("/System/Configuration", "POST", Summary = "Updates application configuration")]
-    [Authenticated(Roles = "Admin")]
-    public class UpdateConfiguration : ServerConfiguration, IReturnVoid
-    {
-    }
-
-    [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")]
-    [Authenticated(Roles = "Admin")]
-    public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream
-    {
-        [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Key { get; set; }
-
-        public Stream RequestStream { get; set; }
-    }
-
-    [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDefaultMetadataOptions : IReturn<MetadataOptions>
-    {
-    }
-
-    [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")]
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class UpdateMediaEncoderPath : IReturnVoid
-    {
-        [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Path { get; set; }
-        [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string PathType { get; set; }
-    }
-
-    public class ConfigurationService : BaseApiService
-    {
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// The _configuration manager.
-        /// </summary>
-        private readonly IServerConfigurationManager _configurationManager;
-
-        private readonly IMediaEncoder _mediaEncoder;
-
-        public ConfigurationService(
-            ILogger<ConfigurationService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IServerConfigurationManager configurationManager,
-            IMediaEncoder mediaEncoder)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _jsonSerializer = jsonSerializer;
-            _configurationManager = configurationManager;
-            _mediaEncoder = mediaEncoder;
-        }
-
-        public void Post(UpdateMediaEncoderPath request)
-        {
-            _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetConfiguration request)
-        {
-            return ToOptimizedResult(_configurationManager.Configuration);
-        }
-
-        public object Get(GetNamedConfiguration request)
-        {
-            var result = _configurationManager.GetConfiguration(request.Key);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified configuraiton.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateConfiguration request)
-        {
-            // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
-            var json = _jsonSerializer.SerializeToString(request);
-
-            var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
-
-            _configurationManager.ReplaceConfiguration(config);
-        }
-
-        public async Task Post(UpdateNamedConfiguration request)
-        {
-            var key = GetPathValue(2).ToString();
-
-            var configurationType = _configurationManager.GetConfigurationType(key);
-            var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false);
-
-            _configurationManager.SaveConfiguration(key, configuration);
-        }
-
-        public object Get(GetDefaultMetadataOptions request)
-        {
-            return ToOptimizedResult(new MetadataOptions());
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs
deleted file mode 100644
index 18860983ec..0000000000
--- a/MediaBrowser.Api/Devices/DeviceService.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Devices
-{
-    [Route("/Devices", "GET", Summary = "Gets all devices")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDevices : DeviceQuery, IReturn<QueryResult<DeviceInfo>>
-    {
-    }
-
-    [Route("/Devices/Info", "GET", Summary = "Gets info for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceInfo : IReturn<DeviceInfo>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/Options", "GET", Summary = "Gets options for a device")]
-    [Authenticated(Roles = "Admin")]
-    public class GetDeviceOptions : IReturn<DeviceOptions>
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices", "DELETE", Summary = "Deletes a device")]
-    public class DeleteDevice
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Devices/Options", "POST", Summary = "Updates device options")]
-    [Authenticated(Roles = "Admin")]
-    public class PostDeviceOptions : DeviceOptions, IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    public class DeviceService : BaseApiService
-    {
-        private readonly IDeviceManager _deviceManager;
-        private readonly IAuthenticationRepository _authRepo;
-        private readonly ISessionManager _sessionManager;
-
-        public DeviceService(
-            ILogger<DeviceService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDeviceManager deviceManager,
-            IAuthenticationRepository authRepo,
-            ISessionManager sessionManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _deviceManager = deviceManager;
-            _authRepo = authRepo;
-            _sessionManager = sessionManager;
-        }
-
-        public void Post(PostDeviceOptions request)
-        {
-            _deviceManager.UpdateDeviceOptions(request.Id, request);
-        }
-
-        public object Get(GetDevices request)
-        {
-            return ToOptimizedResult(_deviceManager.GetDevices(request));
-        }
-
-        public object Get(GetDeviceInfo request)
-        {
-            return _deviceManager.GetDevice(request.Id);
-        }
-
-        public object Get(GetDeviceOptions request)
-        {
-            return _deviceManager.GetDeviceOptions(request.Id);
-        }
-
-        public void Delete(DeleteDevice request)
-        {
-            var sessions = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                DeviceId = request.Id
-            }).Items;
-
-            foreach (var session in sessions)
-            {
-                _sessionManager.Logout(session);
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
deleted file mode 100644
index c3ed40ad3c..0000000000
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Threading;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class UpdateDisplayPreferences.
-    /// </summary>
-    [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
-    public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string DisplayPreferencesId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferences>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Client { get; set; }
-    }
-
-    /// <summary>
-    /// Class DisplayPreferencesService.
-    /// </summary>
-    [Authenticated]
-    public class DisplayPreferencesService : BaseApiService
-    {
-        /// <summary>
-        /// The _display preferences manager.
-        /// </summary>
-        private readonly IDisplayPreferencesRepository _displayPreferencesManager;
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
-        /// </summary>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="displayPreferencesManager">The display preferences manager.</param>
-        public DisplayPreferencesService(
-            ILogger<DisplayPreferencesService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IDisplayPreferencesRepository displayPreferencesManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _jsonSerializer = jsonSerializer;
-            _displayPreferencesManager = displayPreferencesManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Get(GetDisplayPreferences request)
-        {
-            var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateDisplayPreferences request)
-        {
-            // Serialize to json and then back so that the core doesn't see the request dto type
-            var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
-
-            _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs
deleted file mode 100644
index 720a710258..0000000000
--- a/MediaBrowser.Api/EnvironmentService.cs
+++ /dev/null
@@ -1,285 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetDirectoryContents.
-    /// </summary>
-    [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")]
-    public class GetDirectoryContents : IReturn<List<FileSystemEntryInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [include files].
-        /// </summary>
-        /// <value><c>true</c> if [include files]; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "IncludeFiles", Description = "An optional filter to include or exclude files from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool IncludeFiles { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [include directories].
-        /// </summary>
-        /// <value><c>true</c> if [include directories]; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "IncludeDirectories", Description = "An optional filter to include or exclude folders from the results. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool IncludeDirectories { get; set; }
-    }
-
-    [Route("/Environment/ValidatePath", "POST", Summary = "Gets the contents of a given directory in the file system")]
-    public class ValidatePath
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Path { get; set; }
-
-        public bool ValidateWriteable { get; set; }
-
-        public bool? IsFile { get; set; }
-    }
-
-    [Obsolete]
-    [Route("/Environment/NetworkShares", "GET", Summary = "Gets shares from a network device")]
-    public class GetNetworkShares : IReturn<List<FileSystemEntryInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetDrives.
-    /// </summary>
-    [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")]
-    public class GetDrives : IReturn<List<FileSystemEntryInfo>>
-    {
-    }
-
-    /// <summary>
-    /// Class GetNetworkComputers.
-    /// </summary>
-    [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")]
-    public class GetNetworkDevices : IReturn<List<FileSystemEntryInfo>>
-    {
-    }
-
-    [Route("/Environment/ParentPath", "GET", Summary = "Gets the parent path of a given path")]
-    public class GetParentPath : IReturn<string>
-    {
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Path { get; set; }
-    }
-
-    public class DefaultDirectoryBrowserInfo
-    {
-        public string Path { get; set; }
-    }
-
-    [Route("/Environment/DefaultDirectoryBrowser", "GET", Summary = "Gets the parent path of a given path")]
-    public class GetDefaultDirectoryBrowser : IReturn<DefaultDirectoryBrowserInfo>
-    {
-    }
-
-    /// <summary>
-    /// Class EnvironmentService.
-    /// </summary>
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class EnvironmentService : BaseApiService
-    {
-        private const char UncSeparator = '\\';
-        private const string UncSeparatorString = "\\";
-
-        /// <summary>
-        /// The _network manager.
-        /// </summary>
-        private readonly INetworkManager _networkManager;
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="EnvironmentService" /> class.
-        /// </summary>
-        /// <param name="networkManager">The network manager.</param>
-        public EnvironmentService(
-            ILogger<EnvironmentService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            INetworkManager networkManager,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _networkManager = networkManager;
-            _fileSystem = fileSystem;
-        }
-
-        public void Post(ValidatePath request)
-        {
-            if (request.IsFile.HasValue)
-            {
-                if (request.IsFile.Value)
-                {
-                    if (!File.Exists(request.Path))
-                    {
-                        throw new FileNotFoundException("File not found", request.Path);
-                    }
-                }
-                else
-                {
-                    if (!Directory.Exists(request.Path))
-                    {
-                        throw new FileNotFoundException("File not found", request.Path);
-                    }
-                }
-            }
-
-            else
-            {
-                if (!File.Exists(request.Path) && !Directory.Exists(request.Path))
-                {
-                    throw new FileNotFoundException("Path not found", request.Path);
-                }
-
-                if (request.ValidateWriteable)
-                {
-                    EnsureWriteAccess(request.Path);
-                }
-            }
-        }
-
-        protected void EnsureWriteAccess(string path)
-        {
-            var file = Path.Combine(path, Guid.NewGuid().ToString());
-
-            File.WriteAllText(file, string.Empty);
-            _fileSystem.DeleteFile(file);
-        }
-
-        public object Get(GetDefaultDirectoryBrowser request) =>
-            ToOptimizedResult(new DefaultDirectoryBrowserInfo { Path = null });
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetDirectoryContents request)
-        {
-            var path = request.Path;
-
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(Path));
-            }
-
-            var networkPrefix = UncSeparatorString + UncSeparatorString;
-
-            if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase)
-                && path.LastIndexOf(UncSeparator) == 1)
-            {
-                return ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-            }
-
-            return ToOptimizedResult(GetFileSystemEntries(request).ToList());
-        }
-
-        [Obsolete]
-        public object Get(GetNetworkShares request)
-            => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetDrives request)
-        {
-            var result = GetDrives().ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the list that is returned when an empty path is supplied.
-        /// </summary>
-        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
-        private IEnumerable<FileSystemEntryInfo> GetDrives()
-        {
-            return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetNetworkDevices request)
-            => ToOptimizedResult(Array.Empty<FileSystemEntryInfo>());
-
-        /// <summary>
-        /// Gets the file system entries.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
-        private IEnumerable<FileSystemEntryInfo> GetFileSystemEntries(GetDirectoryContents request)
-        {
-            var entries = _fileSystem.GetFileSystemEntries(request.Path).OrderBy(i => i.FullName).Where(i =>
-            {
-                var isDirectory = i.IsDirectory;
-
-                if (!request.IncludeFiles && !isDirectory)
-                {
-                    return false;
-                }
-
-                return request.IncludeDirectories || !isDirectory;
-            });
-
-            return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
-        }
-
-        public object Get(GetParentPath request)
-        {
-            var parent = Path.GetDirectoryName(request.Path);
-
-            if (string.IsNullOrEmpty(parent))
-            {
-                // Check if unc share
-                var index = request.Path.LastIndexOf(UncSeparator);
-
-                if (index != -1 && request.Path.IndexOf(UncSeparator) == 0)
-                {
-                    parent = request.Path.Substring(0, index);
-
-                    if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
-                    {
-                        parent = null;
-                    }
-                }
-            }
-
-            return parent;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs
deleted file mode 100644
index dcfdcbfed3..0000000000
--- a/MediaBrowser.Api/FilterService.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Items/Filters", "GET", Summary = "Gets branding configuration")]
-    public class GetQueryFiltersLegacy : IReturn<QueryFiltersLegacy>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-    }
-
-    [Route("/Items/Filters2", "GET", Summary = "Gets branding configuration")]
-    public class GetQueryFilters : IReturn<QueryFilters>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public bool? IsAiring { get; set; }
-
-        public bool? IsMovie { get; set; }
-
-        public bool? IsSports { get; set; }
-
-        public bool? IsKids { get; set; }
-
-        public bool? IsNews { get; set; }
-
-        public bool? IsSeries { get; set; }
-
-        public bool? Recursive { get; set; }
-    }
-
-    [Authenticated]
-    public class FilterService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserManager _userManager;
-
-        public FilterService(
-            ILogger<FilterService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            IUserManager userManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-        }
-
-        public object Get(GetQueryFilters request)
-        {
-            var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
-            {
-                parentItem = null;
-            }
-
-            var filters = new QueryFilters();
-
-            var genreQuery = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = Array.Empty<ItemFields>(),
-                    EnableImages = false,
-                    EnableUserData = false
-                },
-                IsAiring = request.IsAiring,
-                IsMovie = request.IsMovie,
-                IsSports = request.IsSports,
-                IsKids = request.IsKids,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries
-            };
-
-            // Non recursive not yet supported for library folders
-            if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
-            {
-                genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
-            }
-            else
-            {
-                genreQuery.Parent = parentItem;
-            }
-
-            if (string.Equals(request.IncludeItemTypes, "MusicAlbum", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "MusicVideo", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "MusicArtist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Audio", StringComparison.OrdinalIgnoreCase))
-            {
-                filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
-                {
-                    Name = i.Item1.Name,
-                    Id = i.Item1.Id
-                }).ToArray();
-            }
-            else
-            {
-                filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
-                {
-                    Name = i.Item1.Name,
-                    Id = i.Item1.Id
-                }).ToArray();
-            }
-
-            return ToOptimizedResult(filters);
-        }
-
-        public object Get(GetQueryFiltersLegacy request)
-        {
-            var parentItem = string.IsNullOrEmpty(request.ParentId) ? null : _libraryManager.GetItemById(request.ParentId);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, typeof(Trailer).Name, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(request.IncludeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
-            {
-                parentItem = null;
-            }
-
-            var item = string.IsNullOrEmpty(request.ParentId) ?
-               user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
-               parentItem;
-
-            var result = ((Folder)item).GetItemList(GetItemsQuery(request, user));
-
-            var filters = GetFilters(result);
-
-            return ToOptimizedResult(filters);
-        }
-
-        private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
-        {
-            var result = new QueryFiltersLegacy();
-
-            result.Years = items.Select(i => i.ProductionYear ?? -1)
-                .Where(i => i > 0)
-                .Distinct()
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.Genres = items.SelectMany(i => i.Genres)
-                .DistinctNames()
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.Tags = items
-                .SelectMany(i => i.Tags)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
-                .ToArray();
-
-            result.OfficialRatings = items
-                .Select(i => i.OfficialRating)
-                .Where(i => !string.IsNullOrWhiteSpace(i))
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
-                .ToArray();
-
-            return result;
-        }
-
-        private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, User user)
-        {
-            var query = new InternalItemsQuery
-            {
-                User = user,
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                Recursive = true,
-                EnableTotalRecordCount = false,
-                DtoOptions = new Controller.Dto.DtoOptions
-                {
-                    Fields = new[] { ItemFields.Genres, ItemFields.Tags },
-                    EnableImages = false,
-                    EnableUserData = false
-                }
-            };
-
-            return query;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs
deleted file mode 100644
index 2d405ac3d8..0000000000
--- a/MediaBrowser.Api/Images/ImageByNameService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
-    /// <summary>
-    /// Class GetGeneralImage.
-    /// </summary>
-    [Route("/Images/General/{Name}/{Type}", "GET", Summary = "Gets a general image by name")]
-    public class GetGeneralImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Type", Description = "Image Type (primary, backdrop, logo, etc).", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Type { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetRatingImage.
-    /// </summary>
-    [Route("/Images/Ratings/{Theme}/{Name}", "GET", Summary = "Gets a rating image by name")]
-    public class GetRatingImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the theme.
-        /// </summary>
-        /// <value>The theme.</value>
-        [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Theme { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetMediaInfoImage.
-    /// </summary>
-    [Route("/Images/MediaInfo/{Theme}/{Name}", "GET", Summary = "Gets a media info image by name")]
-    public class GetMediaInfoImage
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the image", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the theme.
-        /// </summary>
-        /// <value>The theme.</value>
-        [ApiMember(Name = "Theme", Description = "The theme to get the image from", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Theme { get; set; }
-    }
-
-    [Route("/Images/MediaInfo", "GET", Summary = "Gets all media info image by name")]
-    [Authenticated]
-    public class GetMediaInfoImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    [Route("/Images/Ratings", "GET", Summary = "Gets all rating images by name")]
-    [Authenticated]
-    public class GetRatingImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    [Route("/Images/General", "GET", Summary = "Gets all general images by name")]
-    [Authenticated]
-    public class GetGeneralImages : IReturn<List<ImageByNameInfo>>
-    {
-    }
-
-    /// <summary>
-    /// Class ImageByNameService.
-    /// </summary>
-    public class ImageByNameService : BaseApiService
-    {
-        /// <summary>
-        /// The _app paths.
-        /// </summary>
-        private readonly IServerApplicationPaths _appPaths;
-
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImageByNameService" /> class.
-        /// </summary>
-        public ImageByNameService(
-            ILogger<ImageByNameService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory resultFactory,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, resultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _fileSystem = fileSystem;
-        }
-
-        public object Get(GetMediaInfoImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath, true));
-        }
-
-        public object Get(GetRatingImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.RatingsPath, true));
-        }
-
-        public object Get(GetGeneralImages request)
-        {
-            return ToOptimizedResult(GetImageList(_appPaths.GeneralPath, false));
-        }
-
-        private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
-        {
-            try
-            {
-                return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
-                    .Select(i => new ImageByNameInfo
-                    {
-                        Name = _fileSystem.GetFileNameWithoutExtension(i),
-                        FileLength = i.Length,
-
-                        // For themeable images, use the Theme property
-                        // For general images, the same object structure is fine,
-                        // but it's not owned by a theme, so call it Context
-                        Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
-                        Context = supportsThemes ? null : GetThemeName(i.FullName, path),
-
-                        Format = i.Extension.ToLowerInvariant().TrimStart('.')
-                    })
-                    .OrderBy(i => i.Name)
-                    .ToList();
-            }
-            catch (IOException)
-            {
-                return new List<ImageByNameInfo>();
-            }
-        }
-
-        private string GetThemeName(string path, string rootImagePath)
-        {
-            var parentName = Path.GetDirectoryName(path);
-
-            if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
-            {
-                return null;
-            }
-
-            parentName = Path.GetFileName(parentName);
-
-            return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ?
-                null :
-                parentName;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetGeneralImage request)
-        {
-            var filename = string.Equals(request.Type, "primary", StringComparison.OrdinalIgnoreCase)
-                               ? "folder"
-                               : request.Type;
-
-            var paths = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(_appPaths.GeneralPath, request.Name, filename + i)).ToList();
-
-            var path = paths.FirstOrDefault(File.Exists) ?? paths.FirstOrDefault();
-
-            return ResultFactory.GetStaticFileResult(Request, path);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRatingImage request)
-        {
-            var themeFolder = Path.Combine(_appPaths.RatingsPath, request.Theme);
-
-            if (Directory.Exists(themeFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions
-                    .Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            var allFolder = Path.Combine(_appPaths.RatingsPath, "all");
-
-            if (Directory.Exists(allFolder))
-            {
-                // Avoid implicitly captured closure
-                var currentRequest = request;
-
-                var path = BaseItem.SupportedImageExtensions
-                    .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetMediaInfoImage request)
-        {
-            var themeFolder = Path.Combine(_appPaths.MediaInfoImagesPath, request.Theme);
-
-            if (Directory.Exists(themeFolder))
-            {
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            var allFolder = Path.Combine(_appPaths.MediaInfoImagesPath, "all");
-
-            if (Directory.Exists(allFolder))
-            {
-                // Avoid implicitly captured closure
-                var currentRequest = request;
-
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
-                    .FirstOrDefault(File.Exists);
-
-                if (!string.IsNullOrEmpty(path))
-                {
-                    return ResultFactory.GetStaticFileResult(Request, path);
-                }
-            }
-
-            throw new ResourceNotFoundException("MediaInfo image not found: " + request.Name);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
deleted file mode 100644
index 86464b4b9a..0000000000
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ /dev/null
@@ -1,296 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Images
-{
-    public class BaseRemoteImageRequest : IReturn<RemoteImageResult>
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public ImageType? Type { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "IncludeAllLanguages", Description = "Optional.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeAllLanguages { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages", "GET", Summary = "Gets available remote images for an item")]
-    [Authenticated]
-    public class GetRemoteImages : BaseRemoteImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Providers", "GET", Summary = "Gets available remote image providers for an item")]
-    [Authenticated]
-    public class GetRemoteImageProviders : IReturn<List<ImageProviderInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    public class BaseDownloadRemoteImage : IReturnVoid
-    {
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public ImageType Type { get; set; }
-
-        [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ProviderName { get; set; }
-
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ImageUrl { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteImages/Download", "POST", Summary = "Downloads a remote image for an item")]
-    [Authenticated(Roles = "Admin")]
-    public class DownloadRemoteImage : BaseDownloadRemoteImage
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Images/Remote", "GET", Summary = "Gets a remote image")]
-    public class GetRemoteImage
-    {
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ImageUrl { get; set; }
-    }
-
-    public class RemoteImageService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IHttpClient _httpClient;
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ILibraryManager _libraryManager;
-
-        public RemoteImageService(
-            ILogger<RemoteImageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            IServerApplicationPaths appPaths,
-            IHttpClient httpClient,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _appPaths = appPaths;
-            _httpClient = httpClient;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetRemoteImageProviders request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var result = GetImageProviders(item);
-
-            return ToOptimizedResult(result);
-        }
-
-        private List<ImageProviderInfo> GetImageProviders(BaseItem item)
-        {
-            return _providerManager.GetRemoteImageProviderInfo(item).ToList();
-        }
-
-        public async Task<object> Get(GetRemoteImages request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery(request.ProviderName)
-            {
-                IncludeAllLanguages = request.IncludeAllLanguages,
-                IncludeDisabledProviders = true,
-                ImageType = request.Type
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            var imagesList = images.ToArray();
-
-            var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
-
-            if (request.Type.HasValue)
-            {
-                allProviders = allProviders.Where(i => i.SupportedImages.Contains(request.Type.Value));
-            }
-
-            var result = new RemoteImageResult
-            {
-                TotalRecordCount = imagesList.Length,
-                Providers = allProviders.Select(i => i.Name)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToArray()
-            };
-
-            if (request.StartIndex.HasValue)
-            {
-                imagesList = imagesList.Skip(request.StartIndex.Value)
-                    .ToArray();
-            }
-
-            if (request.Limit.HasValue)
-            {
-                imagesList = imagesList.Take(request.Limit.Value)
-                    .ToArray();
-            }
-
-            result.Images = imagesList;
-
-            return result;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(DownloadRemoteImage request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return DownloadRemoteImage(item, request);
-        }
-
-        /// <summary>
-        /// Downloads the remote image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="request">The request.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadRemoteImage(BaseItem item, BaseDownloadRemoteImage request)
-        {
-            await _providerManager.SaveImage(item, request.ImageUrl, request.Type, null, CancellationToken.None).ConfigureAwait(false);
-
-            item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetRemoteImage request)
-        {
-            var urlHash = request.ImageUrl.GetMD5();
-            var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
-            string contentPath;
-
-            try
-            {
-                contentPath = File.ReadAllText(pointerCachePath);
-
-                if (File.Exists(contentPath))
-                {
-                    return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // Means the file isn't cached yet
-            }
-            catch (IOException)
-            {
-                // Means the file isn't cached yet
-            }
-
-            await DownloadImage(request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
-            // Read the pointer file again
-            contentPath = File.ReadAllText(pointerCachePath);
-
-            return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Downloads the image.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <param name="urlHash">The URL hash.</param>
-        /// <param name="pointerCachePath">The pointer cache path.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
-        {
-            using var result = await _httpClient.GetResponse(new HttpRequestOptions
-            {
-                Url = url,
-                BufferContent = false
-            }).ConfigureAwait(false);
-            var ext = result.ContentType.Split('/')[^1];
-
-            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            var stream = result.Content;
-            await using (stream.ConfigureAwait(false))
-            {
-                var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-                await using (filestream.ConfigureAwait(false))
-                {
-                    await stream.CopyToAsync(filestream).ConfigureAwait(false);
-                }
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
-            File.WriteAllText(pointerCachePath, fullCachePath);
-        }
-
-        /// <summary>
-        /// Gets the full cache path.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        private string GetFullCachePath(string filename)
-        {
-            return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
deleted file mode 100644
index 8624112099..0000000000
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ /dev/null
@@ -1,336 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Items/{Id}/ExternalIdInfos", "GET", Summary = "Gets external id infos for an item")]
-    [Authenticated(Roles = "Admin")]
-    public class GetExternalIdInfos : IReturn<List<ExternalIdInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-    }
-
-    [Route("/Items/RemoteSearch/Movie", "POST")]
-    [Authenticated]
-    public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Trailer", "POST")]
-    [Authenticated]
-    public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicVideo", "POST")]
-    [Authenticated]
-    public class GetMusicVideoRemoteSearchResults : RemoteSearchQuery<MusicVideoInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Series", "POST")]
-    [Authenticated]
-    public class GetSeriesRemoteSearchResults : RemoteSearchQuery<SeriesInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/BoxSet", "POST")]
-    [Authenticated]
-    public class GetBoxSetRemoteSearchResults : RemoteSearchQuery<BoxSetInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicArtist", "POST")]
-    [Authenticated]
-    public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery<ArtistInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/MusicAlbum", "POST")]
-    [Authenticated]
-    public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery<AlbumInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Person", "POST")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPersonRemoteSearchResults : RemoteSearchQuery<PersonLookupInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Book", "POST")]
-    [Authenticated]
-    public class GetBookRemoteSearchResults : RemoteSearchQuery<BookInfo>, IReturn<List<RemoteSearchResult>>
-    {
-    }
-
-    [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")]
-    public class GetRemoteSearchImage
-    {
-        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ImageUrl { get; set; }
-
-        [ApiMember(Name = "ProviderName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProviderName { get; set; }
-    }
-
-    [Route("/Items/RemoteSearch/Apply/{Id}", "POST", Summary = "Applies search criteria to an item and refreshes metadata")]
-    [Authenticated(Roles = "Admin")]
-    public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "ReplaceAllImages", Description = "Whether or not to replace all images", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool ReplaceAllImages { get; set; }
-
-        public ApplySearchCriteria()
-        {
-            ReplaceAllImages = true;
-        }
-    }
-
-    public class ItemLookupService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IJsonSerializer _json;
-
-        public ItemLookupService(
-            ILogger<ItemLookupService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager,
-            IJsonSerializer json)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-            _json = json;
-        }
-
-        public object Get(GetExternalIdInfos request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var infos = _providerManager.GetExternalIdInfos(item).ToList();
-
-            return ToOptimizedResult(infos);
-        }
-
-        public async Task<object> Post(GetTrailerRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetBookRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMovieRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetSeriesRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetBoxSetRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicVideoRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetPersonRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicAlbumRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(GetMusicArtistRemoteSearchResults request)
-        {
-            var result = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task<object> Get(GetRemoteSearchImage request)
-        {
-            return GetRemoteImage(request);
-        }
-
-        public Task Post(ApplySearchCriteria request)
-        {
-            var item = _libraryManager.GetItemById(new Guid(request.Id));
-
-            // foreach (var key in request.ProviderIds)
-            //{
-            //    var value = key.Value;
-
-            //    if (!string.IsNullOrWhiteSpace(value))
-            //    {
-            //        item.SetProviderId(key.Key, value);
-            //    }
-            //}
-            Logger.LogInformation("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds));
-
-            // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
-            item.ProviderIds = request.ProviderIds;
-            // item.ProductionYear = request.ProductionYear;
-            // item.Name = request.Name;
-
-            return _providerManager.RefreshFullItem(
-                item,
-                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                {
-                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ReplaceAllMetadata = true,
-                    ReplaceAllImages = request.ReplaceAllImages,
-                    SearchResult = request
-                },
-                CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Gets the remote image.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetRemoteImage(GetRemoteSearchImage request)
-        {
-            var urlHash = request.ImageUrl.GetMD5();
-            var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
-            string contentPath;
-
-            try
-            {
-                contentPath = File.ReadAllText(pointerCachePath);
-
-                if (File.Exists(contentPath))
-                {
-                    return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // Means the file isn't cached yet
-            }
-            catch (IOException)
-            {
-                // Means the file isn't cached yet
-            }
-
-            await DownloadImage(request.ProviderName, request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
-            // Read the pointer file again
-            contentPath = File.ReadAllText(pointerCachePath);
-
-            return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Downloads the image.
-        /// </summary>
-        /// <param name="providerName">Name of the provider.</param>
-        /// <param name="url">The URL.</param>
-        /// <param name="urlHash">The URL hash.</param>
-        /// <param name="pointerCachePath">The pointer cache path.</param>
-        /// <returns>Task.</returns>
-        private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
-        {
-            var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
-
-            var ext = result.ContentType.Split('/')[^1];
-
-            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
-            var stream = result.Content;
-
-            await using (stream.ConfigureAwait(false))
-            {
-                var fileStream = new FileStream(
-                    fullCachePath,
-                    FileMode.Create,
-                    FileAccess.Write,
-                    FileShare.Read,
-                    IODefaults.FileStreamBufferSize,
-                    true);
-                await using (fileStream.ConfigureAwait(false))
-                {
-                    await stream.CopyToAsync(fileStream).ConfigureAwait(false);
-                }
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
-            File.WriteAllText(pointerCachePath, fullCachePath);
-        }
-
-        /// <summary>
-        /// Gets the full cache path.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.String.</returns>
-        private string GetFullCachePath(string filename)
-            => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-    }
-}
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
deleted file mode 100644
index 6555864dc5..0000000000
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ /dev/null
@@ -1,1124 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Api.Movies;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-
-namespace MediaBrowser.Api.Library
-{
-    [Route("/Items/{Id}/File", "GET", Summary = "Gets the original file of an item")]
-    [Authenticated]
-    public class GetFile
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetCriticReviews.
-    /// </summary>
-    [Route("/Items/{Id}/CriticReviews", "GET", Summary = "Gets critic reviews for an item")]
-    [Authenticated]
-    public class GetCriticReviews : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeSongs.
-    /// </summary>
-    [Route("/Items/{Id}/ThemeSongs", "GET", Summary = "Gets theme songs for an item")]
-    [Authenticated]
-    public class GetThemeSongs : IReturn<ThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeVideos.
-    /// </summary>
-    [Route("/Items/{Id}/ThemeVideos", "GET", Summary = "Gets theme videos for an item")]
-    [Authenticated]
-    public class GetThemeVideos : IReturn<ThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetThemeVideos.
-    /// </summary>
-    [Route("/Items/{Id}/ThemeMedia", "GET", Summary = "Gets theme videos and songs for an item")]
-    [Authenticated]
-    public class GetThemeMedia : IReturn<AllThemeMediaResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "InheritFromParent", Description = "Determines whether or not parent items should be searched for theme media.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool InheritFromParent { get; set; }
-    }
-
-    [Route("/Library/Refresh", "POST", Summary = "Starts a library scan")]
-    [Authenticated(Roles = "Admin")]
-    public class RefreshLibrary : IReturnVoid
-    {
-    }
-
-    [Route("/Items/{Id}", "DELETE", Summary = "Deletes an item from the library and file system")]
-    [Authenticated]
-    public class DeleteItem : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items", "DELETE", Summary = "Deletes an item from the library and file system")]
-    [Authenticated]
-    public class DeleteItems : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Ids", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Ids { get; set; }
-    }
-
-    [Route("/Items/Counts", "GET")]
-    [Authenticated]
-    public class GetItemCounts : IReturn<ItemCounts>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional. Get counts from a specific user's library.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "IsFavorite", Description = "Optional. Get counts of favorite items", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-    }
-
-    [Route("/Items/{Id}/Ancestors", "GET", Summary = "Gets all parents of an item")]
-    [Authenticated]
-    public class GetAncestors : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPhyscialPaths.
-    /// </summary>
-    [Route("/Library/PhysicalPaths", "GET", Summary = "Gets a list of physical paths from virtual folders")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPhyscialPaths : IReturn<List<string>>
-    {
-    }
-
-    [Route("/Library/MediaFolders", "GET", Summary = "Gets all user media folders.")]
-    [Authenticated]
-    public class GetMediaFolders : IReturn<QueryResult<BaseItemDto>>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-    }
-
-    [Route("/Library/Series/Added", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
-    [Route("/Library/Series/Updated", "POST", Summary = "Reports that new episodes of a series have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedSeries : IReturnVoid
-    {
-        [ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string TvdbId { get; set; }
-    }
-
-    [Route("/Library/Movies/Added", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Route("/Library/Movies/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedMovies : IReturnVoid
-    {
-        [ApiMember(Name = "TmdbId", Description = "Tmdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string TmdbId { get; set; }
-        [ApiMember(Name = "ImdbId", Description = "Imdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string ImdbId { get; set; }
-    }
-
-    public class MediaUpdateInfo
-    {
-        public string Path { get; set; }
-
-        // Created, Modified, Deleted
-        public string UpdateType { get; set; }
-    }
-
-    [Route("/Library/Media/Updated", "POST", Summary = "Reports that new movies have been added by an external source")]
-    [Authenticated]
-    public class PostUpdatedMedia : IReturnVoid
-    {
-        [ApiMember(Name = "Updates", Description = "A list of updated media paths", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public List<MediaUpdateInfo> Updates { get; set; }
-    }
-
-    [Route("/Items/{Id}/Download", "GET", Summary = "Downloads item media")]
-    [Authenticated(Roles = "download")]
-    public class GetDownload
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    [Route("/Items/{Id}/Similar", "GET", Summary = "Gets similar items")]
-    [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")]
-    [Route("/Movies/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given movie.")]
-    [Route("/Trailers/{Id}/Similar", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
-    [Authenticated]
-    public class GetSimilarItems : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Libraries/AvailableOptions", "GET")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetLibraryOptionsInfo : IReturn<LibraryOptionsResult>
-    {
-        public string LibraryContentType { get; set; }
-
-        public bool IsNewLibrary { get; set; }
-    }
-
-    public class LibraryOptionInfo
-    {
-        public string Name { get; set; }
-
-        public bool DefaultEnabled { get; set; }
-    }
-
-    public class LibraryOptionsResult
-    {
-        public LibraryOptionInfo[] MetadataSavers { get; set; }
-
-        public LibraryOptionInfo[] MetadataReaders { get; set; }
-
-        public LibraryOptionInfo[] SubtitleFetchers { get; set; }
-
-        public LibraryTypeOptions[] TypeOptions { get; set; }
-    }
-
-    public class LibraryTypeOptions
-    {
-        public string Type { get; set; }
-
-        public LibraryOptionInfo[] MetadataFetchers { get; set; }
-
-        public LibraryOptionInfo[] ImageFetchers { get; set; }
-
-        public ImageType[] SupportedImageTypes { get; set; }
-
-        public ImageOption[] DefaultImageOptions { get; set; }
-    }
-
-    /// <summary>
-    /// Class LibraryService.
-    /// </summary>
-    public class LibraryService : BaseApiService
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IUserManager _userManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IActivityManager _activityManager;
-        private readonly ILocalizationManager _localization;
-        private readonly ILibraryMonitor _libraryMonitor;
-
-        private readonly ILogger<MoviesService> _moviesServiceLogger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryService" /> class.
-        /// </summary>
-        public LibraryService(
-            ILogger<LibraryService> logger,
-            ILogger<MoviesService> moviesServiceLogger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IProviderManager providerManager,
-            ILibraryManager libraryManager,
-            IUserManager userManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            IActivityManager activityManager,
-            ILocalizationManager localization,
-            ILibraryMonitor libraryMonitor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _providerManager = providerManager;
-            _libraryManager = libraryManager;
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _activityManager = activityManager;
-            _localization = localization;
-            _libraryMonitor = libraryMonitor;
-            _moviesServiceLogger = moviesServiceLogger;
-        }
-
-        // Content Types available for each Library
-        private string[] GetRepresentativeItemTypes(string contentType)
-        {
-            return contentType switch
-            {
-                CollectionType.BoxSets => new[] {"BoxSet"},
-                CollectionType.Playlists => new[] {"Playlist"},
-                CollectionType.Movies => new[] {"Movie"},
-                CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
-                CollectionType.Books => new[] {"Book"},
-                CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
-                CollectionType.HomeVideos => new[] {"Video", "Photo"},
-                CollectionType.Photos => new[] {"Video", "Photo"},
-                CollectionType.MusicVideos => new[] {"MusicVideo"},
-                _ => new[] {"Series", "Season", "Episode", "Movie"}
-            };
-        }
-
-        private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                return false;
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
-                .ToArray();
-
-            if (metadataOptions.Length == 0)
-            {
-                return true;
-            }
-
-            return metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
-                {
-                    return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
-                         || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
-                         || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
-                }
-
-                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
-                   || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
-                   || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                .ToArray();
-
-            return metadataOptions.Length == 0
-               || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
-        {
-            if (isNewLibrary)
-            {
-                if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
-                {
-                    return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
-                           && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
-                }
-
-                return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
-                       || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
-            }
-
-            var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
-                .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                .ToArray();
-
-            if (metadataOptions.Length == 0)
-            {
-                return true;
-            }
-
-            return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
-        }
-
-        public object Get(GetLibraryOptionsInfo request)
-        {
-            var result = new LibraryOptionsResult();
-
-            var types = GetRepresentativeItemTypes(request.LibraryContentType);
-            var isNewLibrary = request.IsNewLibrary;
-            var typesList = types.ToList();
-
-            var plugins = _providerManager.GetAllMetadataPlugins()
-                .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
-                .OrderBy(i => typesList.IndexOf(i.ItemType))
-                .ToList();
-
-            result.MetadataSavers = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            result.MetadataReaders = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = true
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            result.SubtitleFetchers = plugins
-                .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
-                .Select(i => new LibraryOptionInfo
-                {
-                    Name = i.Name,
-                    DefaultEnabled = true
-                })
-                .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                .Select(x => x.First())
-                .ToArray();
-
-            var typeOptions = new List<LibraryTypeOptions>();
-
-            foreach (var type in types)
-            {
-                TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
-
-                typeOptions.Add(new LibraryTypeOptions
-                {
-                    Type = type,
-
-                    MetadataFetchers = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
-                    .Select(i => new LibraryOptionInfo
-                    {
-                        Name = i.Name,
-                        DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
-                    })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
-                    .ToArray(),
-
-                    ImageFetchers = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
-                    .Select(i => new LibraryOptionInfo
-                    {
-                        Name = i.Name,
-                        DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
-                    })
-                    .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
-                    .Select(x => x.First())
-                    .ToArray(),
-
-                    SupportedImageTypes = plugins
-                    .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
-                    .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
-                    .Distinct()
-                    .ToArray(),
-
-                    DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
-                });
-            }
-
-            result.TypeOptions = typeOptions.ToArray();
-
-            return result;
-        }
-
-        public object Get(GetSimilarItems request)
-        {
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var program = item as IHasProgramAttributes;
-
-            if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
-            {
-                return new MoviesService(
-                    _moviesServiceLogger,
-                    ServerConfigurationManager,
-                    ResultFactory,
-                    _userManager,
-                    _libraryManager,
-                    _dtoService,
-                    _authContext)
-                {
-                    Request = Request,
-                }.GetSimilarItemsResult(request);
-            }
-
-            if (program != null && program.IsSeries)
-            {
-                return GetSimilarItemsResult(request, new[] { typeof(Series).Name });
-            }
-
-            if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
-            {
-                return new QueryResult<BaseItemDto>();
-            }
-
-            return GetSimilarItemsResult(request, new[] { item.GetType().Name });
-        }
-
-        private QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, string[] includeItemTypes)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                IncludeItemTypes = includeItemTypes,
-                SimilarTo = item,
-                DtoOptions = dtoOptions,
-                EnableTotalRecordCount = false
-            };
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
-            {
-                query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
-            }
-
-            List<BaseItem> itemsResult;
-
-            if (item is MusicArtist)
-            {
-                query.IncludeItemTypes = Array.Empty<string>();
-
-                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
-            }
-            else
-            {
-                itemsResult = _libraryManager.GetItemList(query);
-            }
-
-            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnList,
-                TotalRecordCount = itemsResult.Count
-            };
-
-            return result;
-        }
-
-        public object Get(GetMediaFolders request)
-        {
-            var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
-
-            if (request.IsHidden.HasValue)
-            {
-                var val = request.IsHidden.Value;
-
-                items = items.Where(i => i.IsHidden == val).ToList();
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = items.Count,
-
-                Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray()
-            };
-
-            return result;
-        }
-
-        public void Post(PostUpdatedSeries request)
-        {
-            var series = _libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Series).Name },
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-            }).Where(i => string.Equals(request.TvdbId, i.GetProviderId(MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            foreach (var item in series)
-            {
-                _libraryMonitor.ReportFileSystemChanged(item.Path);
-            }
-        }
-
-        public void Post(PostUpdatedMedia request)
-        {
-            if (request.Updates != null)
-            {
-                foreach (var item in request.Updates)
-                {
-                    _libraryMonitor.ReportFileSystemChanged(item.Path);
-                }
-            }
-        }
-
-        public void Post(PostUpdatedMovies request)
-        {
-            var movies = _libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Movie).Name },
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-            });
-
-            if (!string.IsNullOrWhiteSpace(request.ImdbId))
-            {
-                movies = movies.Where(i => string.Equals(request.ImdbId, i.GetProviderId(MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-            else if (!string.IsNullOrWhiteSpace(request.TmdbId))
-            {
-                movies = movies.Where(i => string.Equals(request.TmdbId, i.GetProviderId(MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-            else
-            {
-                movies = new List<BaseItem>();
-            }
-
-            foreach (var item in movies)
-            {
-                _libraryMonitor.ReportFileSystemChanged(item.Path);
-            }
-        }
-
-        public Task<object> Get(GetDownload request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            var user = auth.User;
-
-            if (user != null)
-            {
-                if (!item.CanDownload(user))
-                {
-                    throw new ArgumentException("Item does not support downloading");
-                }
-            }
-            else
-            {
-                if (!item.CanDownload())
-                {
-                    throw new ArgumentException("Item does not support downloading");
-                }
-            }
-
-            var headers = new Dictionary<string, string>();
-
-            if (user != null)
-            {
-                LogDownload(item, user, auth);
-            }
-
-            var path = item.Path;
-
-            // Quotes are valid in linux. They'll possibly cause issues here
-            var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
-            if (!string.IsNullOrWhiteSpace(filename))
-            {
-                // Kestrel doesn't support non-ASCII characters in headers
-                if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
-                {
-                    // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
-                    headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
-                }
-                else
-                {
-                    headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
-                }
-            }
-
-            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                Path = path,
-                ResponseHeaders = headers
-            });
-        }
-
-        private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
-        {
-            try
-            {
-                _activityManager.Create(new ActivityLog(
-                    string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
-                    "UserDownloadingContent",
-                    auth.UserId)
-                {
-                    ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
-                });
-            }
-            catch
-            {
-                // Logged at lower levels
-            }
-        }
-
-        public Task<object> Get(GetFile request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return ResultFactory.GetStaticFileResult(Request, item.Path);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPhyscialPaths request)
-        {
-            var result = _libraryManager.RootFolder.Children
-                .SelectMany(c => c.PhysicalLocations)
-                .ToList();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetAncestors request)
-        {
-            var result = GetAncestors(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the ancestors.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto[]}.</returns>
-        public List<BaseItemDto> GetAncestors(GetAncestors request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var baseItemDtos = new List<BaseItemDto>();
-
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            BaseItem parent = item.GetParent();
-
-            while (parent != null)
-            {
-                if (user != null)
-                {
-                    parent = TranslateParentItem(parent, user);
-                }
-
-                baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
-
-                parent = parent.GetParent();
-            }
-
-            return baseItemDtos;
-        }
-
-        private BaseItem TranslateParentItem(BaseItem item, User user)
-        {
-            return item.GetParent() is AggregateFolder
-                ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
-                    .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
-                : item;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCriticReviews request)
-        {
-            return new QueryResult<BaseItemDto>();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItemCounts request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var counts = new ItemCounts
-            {
-                AlbumCount = GetCount(typeof(MusicAlbum), user, request),
-                EpisodeCount = GetCount(typeof(Episode), user, request),
-                MovieCount = GetCount(typeof(Movie), user, request),
-                SeriesCount = GetCount(typeof(Series), user, request),
-                SongCount = GetCount(typeof(Audio), user, request),
-                MusicVideoCount = GetCount(typeof(MusicVideo), user, request),
-                BoxSetCount = GetCount(typeof(BoxSet), user, request),
-                BookCount = GetCount(typeof(Book), user, request)
-            };
-
-            return ToOptimizedResult(counts);
-        }
-
-        private int GetCount(Type type, User user, GetItemCounts request)
-        {
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[] { type.Name },
-                Limit = 0,
-                Recursive = true,
-                IsVirtualItem = false,
-                IsFavorite = request.IsFavorite,
-                DtoOptions = new DtoOptions(false)
-                {
-                    EnableImages = false
-                }
-            };
-
-            return _libraryManager.GetItemsResult(query).TotalRecordCount;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(RefreshLibrary request)
-        {
-            try
-            {
-                await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error refreshing library");
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteItems request)
-        {
-            var ids = string.IsNullOrWhiteSpace(request.Ids)
-                ? Array.Empty<string>()
-                : request.Ids.Split(',');
-
-            foreach (var i in ids)
-            {
-                var item = _libraryManager.GetItemById(i);
-                var auth = _authContext.GetAuthorizationInfo(Request);
-                var user = auth.User;
-
-                if (!item.CanDelete(user))
-                {
-                    if (ids.Length > 1)
-                    {
-                        throw new SecurityException("Unauthorized access");
-                    }
-
-                    continue;
-                }
-
-                _libraryManager.DeleteItem(item, new DeleteOptions
-                {
-                    DeleteFileLocation = true
-                }, true);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(DeleteItem request)
-        {
-            Delete(new DeleteItems
-            {
-                Ids = request.Id
-            });
-        }
-
-        public object Get(GetThemeMedia request)
-        {
-            var themeSongs = GetThemeSongs(new GetThemeSongs
-            {
-                InheritFromParent = request.InheritFromParent,
-                Id = request.Id,
-                UserId = request.UserId
-
-            });
-
-            var themeVideos = GetThemeVideos(new GetThemeVideos
-            {
-                InheritFromParent = request.InheritFromParent,
-                Id = request.Id,
-                UserId = request.UserId
-
-            });
-
-            return ToOptimizedResult(new AllThemeMediaResult
-            {
-                ThemeSongsResult = themeSongs,
-                ThemeVideosResult = themeVideos,
-
-                SoundtrackSongsResult = new ThemeMediaResult()
-            });
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetThemeSongs request)
-        {
-            var result = GetThemeSongs(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        private ThemeMediaResult GetThemeSongs(GetThemeSongs request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id)
-                           ? (!request.UserId.Equals(Guid.Empty)
-                                  ? _libraryManager.GetUserRootFolder()
-                                  : _libraryManager.RootFolder)
-                           : _libraryManager.GetItemById(request.Id);
-
-            if (item == null)
-            {
-                throw new ResourceNotFoundException("Item not found.");
-            }
-
-            IEnumerable<BaseItem> themeItems;
-
-            while (true)
-            {
-                themeItems = item.GetThemeSongs();
-
-                if (themeItems.Any() || !request.InheritFromParent)
-                {
-                    break;
-                }
-
-                var parent = item.GetParent();
-                if (parent == null)
-                {
-                    break;
-                }
-
-                item = parent;
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var items = themeItems
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            return new ThemeMediaResult
-            {
-                Items = items,
-                TotalRecordCount = items.Length,
-                OwnerId = item.Id
-            };
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetThemeVideos request)
-        {
-            return ToOptimizedResult(GetThemeVideos(request));
-        }
-
-        public ThemeMediaResult GetThemeVideos(GetThemeVideos request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id)
-                           ? (!request.UserId.Equals(Guid.Empty)
-                                  ? _libraryManager.GetUserRootFolder()
-                                  : _libraryManager.RootFolder)
-                           : _libraryManager.GetItemById(request.Id);
-
-            if (item == null)
-            {
-                throw new ResourceNotFoundException("Item not found.");
-            }
-
-            IEnumerable<BaseItem> themeItems;
-
-            while (true)
-            {
-                themeItems = item.GetThemeVideos();
-
-                if (themeItems.Any() || !request.InheritFromParent)
-                {
-                    break;
-                }
-
-                var parent = item.GetParent();
-                if (parent == null)
-                {
-                    break;
-                }
-
-                item = parent;
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = themeItems
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            return new ThemeMediaResult
-            {
-                Items = items,
-                TotalRecordCount = items.Length,
-                OwnerId = item.Id
-            };
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs
deleted file mode 100644
index b69550ed1a..0000000000
--- a/MediaBrowser.Api/Library/LibraryStructureService.cs
+++ /dev/null
@@ -1,412 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Library
-{
-    /// <summary>
-    /// Class GetDefaultVirtualFolders.
-    /// </summary>
-    [Route("/Library/VirtualFolders", "GET")]
-    public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        public string UserId { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders", "POST")]
-    public class AddVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the collection.
-        /// </summary>
-        /// <value>The type of the collection.</value>
-        public string CollectionType { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public string[] Paths { get; set; }
-
-        public LibraryOptions LibraryOptions { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders", "DELETE")]
-    public class RemoveVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Name", "POST")]
-    public class RenameVirtualFolder : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string NewName { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths", "POST")]
-    public class AddMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Path { get; set; }
-
-        public MediaPathInfo PathInfo { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths/Update", "POST")]
-    public class UpdateMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        public MediaPathInfo PathInfo { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/Paths", "DELETE")]
-    public class RemoveMediaPath : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh library].
-        /// </summary>
-        /// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
-        public bool RefreshLibrary { get; set; }
-    }
-
-    [Route("/Library/VirtualFolders/LibraryOptions", "POST")]
-    public class UpdateLibraryOptions : IReturnVoid
-    {
-        public string Id { get; set; }
-
-        public LibraryOptions LibraryOptions { get; set; }
-    }
-
-    /// <summary>
-    /// Class LibraryStructureService.
-    /// </summary>
-    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
-    public class LibraryStructureService : BaseApiService
-    {
-        /// <summary>
-        /// The _app paths.
-        /// </summary>
-        private readonly IServerApplicationPaths _appPaths;
-
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILibraryMonitor _libraryMonitor;
-
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryStructureService" /> class.
-        /// </summary>
-        public LibraryStructureService(
-            ILogger<LibraryStructureService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            ILibraryMonitor libraryMonitor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _libraryManager = libraryManager;
-            _libraryMonitor = libraryMonitor;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetVirtualFolders request)
-        {
-            var result = _libraryManager.GetVirtualFolders(true);
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Post(UpdateLibraryOptions request)
-        {
-            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
-
-            collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(AddVirtualFolder request)
-        {
-            var libraryOptions = request.LibraryOptions ?? new LibraryOptions();
-
-            if (request.Paths != null && request.Paths.Length > 0)
-            {
-                libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
-            }
-
-            return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RenameVirtualFolder request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            if (string.IsNullOrWhiteSpace(request.NewName))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            var rootFolderPath = _appPaths.DefaultUserViewsPath;
-
-            var currentPath = Path.Combine(rootFolderPath, request.Name);
-            var newPath = Path.Combine(rootFolderPath, request.NewName);
-
-            if (!Directory.Exists(currentPath))
-            {
-                throw new FileNotFoundException("The media collection does not exist");
-            }
-
-            if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
-            {
-                throw new ArgumentException("Media library already exists at " + newPath + ".");
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                // Changing capitalization. Handle windows case insensitivity
-                if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
-                {
-                    var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
-                    Directory.Move(currentPath, tempPath);
-                    currentPath = tempPath;
-                }
-
-                Directory.Move(currentPath, newPath);
-            }
-            finally
-            {
-                CollectionFolder.OnCollectionFolderChange();
-
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(RemoveVirtualFolder request)
-        {
-            return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(AddMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                var mediaPath = request.PathInfo ?? new MediaPathInfo
-                {
-                    Path = request.Path
-                };
-
-                _libraryManager.AddMediaPath(request.Name, mediaPath);
-            }
-            finally
-            {
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryManager.UpdateMediaPath(request.Name, request.PathInfo);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(RemoveMediaPath request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                _libraryManager.RemoveMediaPath(request.Name, request.Path);
-            }
-            finally
-            {
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs
deleted file mode 100644
index 830372dd8e..0000000000
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ /dev/null
@@ -1,1287 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Api.LiveTv
-{
-    /// <summary>
-    /// This is insecure right now to avoid windows phone refactoring.
-    /// </summary>
-    [Route("/LiveTv/Info", "GET", Summary = "Gets available live tv services.")]
-    [Authenticated]
-    public class GetLiveTvInfo : IReturn<LiveTvInfo>
-    {
-    }
-
-    [Route("/LiveTv/Channels", "GET", Summary = "Gets available live tv channels.")]
-    [Authenticated]
-    public class GetChannels : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public ChannelType? Type { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "IsFavorite", Description = "Filter by channels that are favorites, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-
-        [ApiMember(Name = "IsLiked", Description = "Filter by channels that are liked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsLiked { get; set; }
-
-        [ApiMember(Name = "IsDisliked", Description = "Filter by channels that are disliked, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsDisliked { get; set; }
-
-        [ApiMember(Name = "EnableFavoriteSorting", Description = "Incorporate favorite and like status into channel sorting.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool EnableFavoriteSorting { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool AddCurrentProgram { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public string SortBy { get; set; }
-
-        public SortOrder? SortOrder { get; set; }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public string[] GetOrderBy()
-        {
-            var val = SortBy;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return Array.Empty<string>();
-            }
-
-            return val.Split(',');
-        }
-
-        public GetChannels()
-        {
-            AddCurrentProgram = true;
-        }
-    }
-
-    [Route("/LiveTv/Channels/{Id}", "GET", Summary = "Gets a live tv channel")]
-    [Authenticated]
-    public class GetChannel : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings", "GET", Summary = "Gets live tv recordings")]
-    [Authenticated]
-    public class GetRecordings : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public RecordingStatus? Status { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsInProgress { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public bool? IsMovie { get; set; }
-
-        public bool? IsSeries { get; set; }
-
-        public bool? IsKids { get; set; }
-
-        public bool? IsSports { get; set; }
-
-        public bool? IsNews { get; set; }
-
-        public bool? IsLibraryItem { get; set; }
-
-        public GetRecordings()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Recordings/Series", "GET", Summary = "Gets live tv recordings")]
-    [Authenticated]
-    public class GetRecordingSeries : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "GroupId", Description = "Optional filter by recording group.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string GroupId { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recording status.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public RecordingStatus? Status { get; set; }
-
-        [ApiMember(Name = "Status", Description = "Optional filter by recordings that are in progress, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsInProgress { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by recordings belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public GetRecordingSeries()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Recordings/Groups", "GET", Summary = "Gets live tv recording groups")]
-    [Authenticated]
-    public class GetRecordingGroups : IReturn<QueryResult<BaseItemDto>>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings/Folders", "GET", Summary = "Gets recording folders")]
-    [Authenticated]
-    public class GetRecordingFolders : IReturn<BaseItemDto[]>
-    {
-        [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Recordings/{Id}", "GET", Summary = "Gets a live tv recording")]
-    [Authenticated]
-    public class GetRecording : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/LiveTv/Tuners/{Id}/Reset", "POST", Summary = "Resets a tv tuner")]
-    [Authenticated]
-    public class ResetTuner : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Tuner Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "GET", Summary = "Gets a live tv timer")]
-    [Authenticated]
-    public class GetTimer : IReturn<TimerInfoDto>
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/Defaults", "GET", Summary = "Gets default values for a new timer")]
-    [Authenticated]
-    public class GetDefaultTimer : IReturn<SeriesTimerInfoDto>
-    {
-        [ApiMember(Name = "ProgramId", Description = "Optional, to attach default values based on a program.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ProgramId { get; set; }
-    }
-
-    [Route("/LiveTv/Timers", "GET", Summary = "Gets live tv timers")]
-    [Authenticated]
-    public class GetTimers : IReturn<QueryResult<TimerInfoDto>>
-    {
-        [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ChannelId { get; set; }
-
-        [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by timers belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesTimerId { get; set; }
-
-        public bool? IsActive { get; set; }
-
-        public bool? IsScheduled { get; set; }
-    }
-
-    [Route("/LiveTv/Programs", "GET,POST", Summary = "Gets available live tv epgs..")]
-    [Authenticated]
-    public class GetPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "ChannelIds", Description = "The channels to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string ChannelIds { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "MinStartDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MinStartDate { get; set; }
-
-        [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasAired { get; set; }
-
-        public bool? IsAiring { get; set; }
-
-        [ApiMember(Name = "MaxStartDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MaxStartDate { get; set; }
-
-        [ApiMember(Name = "MinEndDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MinEndDate { get; set; }
-
-        [ApiMember(Name = "MaxEndDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string MaxEndDate { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Name, StartDate", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        [ApiMember(Name = "Genres", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string Genres { get; set; }
-
-        [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string GenreIds { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public string SeriesTimerId { get; set; }
-
-        public Guid LibrarySeriesId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        public GetPrograms()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")]
-    [Authenticated]
-    public class GetRecommendedPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        public bool EnableTotalRecordCount { get; set; }
-
-        public GetRecommendedPrograms()
-        {
-            EnableTotalRecordCount = true;
-        }
-
-        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "IsAiring", Description = "Optional. Filter by programs that are currently airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsAiring { get; set; }
-
-        [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasAired { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "GenreIds", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string GenreIds { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
-    [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")]
-    [Authenticated]
-    public class GetProgram : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-
-    [Route("/LiveTv/Recordings/{Id}", "DELETE", Summary = "Deletes a live tv recording")]
-    [Authenticated]
-    public class DeleteRecording : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "DELETE", Summary = "Cancels a live tv timer")]
-    [Authenticated]
-    public class CancelTimer : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/Timers/{Id}", "POST", Summary = "Updates a live tv timer")]
-    [Authenticated]
-    public class UpdateTimer : TimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/Timers", "POST", Summary = "Creates a live tv timer")]
-    [Authenticated]
-    public class CreateTimer : TimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "GET", Summary = "Gets a live tv series timer")]
-    [Authenticated]
-    public class GetSeriesTimer : IReturn<TimerInfoDto>
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers", "GET", Summary = "Gets live tv series timers")]
-    [Authenticated]
-    public class GetSeriesTimers : IReturn<QueryResult<SeriesTimerInfoDto>>
-    {
-        [ApiMember(Name = "SortBy", Description = "Optional. Sort by SortName or Priority", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Optional. Sort in Ascending or Descending order", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
-        public SortOrder SortOrder { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "DELETE", Summary = "Cancels a live tv series timer")]
-    [Authenticated]
-    public class CancelSeriesTimer : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/SeriesTimers/{Id}", "POST", Summary = "Updates a live tv series timer")]
-    [Authenticated]
-    public class UpdateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/SeriesTimers", "POST", Summary = "Creates a live tv series timer")]
-    [Authenticated]
-    public class CreateSeriesTimer : SeriesTimerInfoDto, IReturnVoid
-    {
-    }
-
-    [Route("/LiveTv/Recordings/Groups/{Id}", "GET", Summary = "Gets a recording group")]
-    [Authenticated]
-    public class GetRecordingGroup : IReturn<BaseItemDto>
-    {
-        [ApiMember(Name = "Id", Description = "Recording group Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/GuideInfo", "GET", Summary = "Gets guide info")]
-    [Authenticated]
-    public class GetGuideInfo : IReturn<GuideInfo>
-    {
-    }
-
-    [Route("/LiveTv/TunerHosts", "POST", Summary = "Adds a tuner host")]
-    [Authenticated]
-    public class AddTunerHost : TunerHostInfo, IReturn<TunerHostInfo>
-    {
-    }
-
-    [Route("/LiveTv/TunerHosts", "DELETE", Summary = "Deletes a tuner host")]
-    [Authenticated]
-    public class DeleteTunerHost : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Tuner host id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/Default", "GET")]
-    [Authenticated]
-    public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
-    {
-    }
-
-    [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
-    [Authenticated]
-    public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
-    {
-        public bool ValidateLogin { get; set; }
-
-        public bool ValidateListings { get; set; }
-
-        public string Pw { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
-    [Authenticated]
-    public class DeleteListingProvider : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
-    [Authenticated]
-    public class GetLineups : IReturn<List<NameIdPair>>
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Type", Description = "Provider Type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Type { get; set; }
-
-        [ApiMember(Name = "Location", Description = "Location", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Location { get; set; }
-
-        [ApiMember(Name = "Country", Description = "Country", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Country { get; set; }
-    }
-
-    [Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")]
-    [Authenticated]
-    public class GetSchedulesDirectCountries
-    {
-    }
-
-    [Route("/LiveTv/ChannelMappingOptions")]
-    [Authenticated]
-    public class GetChannelMappingOptions
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
-        public string ProviderId { get; set; }
-    }
-
-    [Route("/LiveTv/ChannelMappings")]
-    [Authenticated]
-    public class SetChannelMapping
-    {
-        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query")]
-        public string ProviderId { get; set; }
-
-        public string TunerChannelId { get; set; }
-
-        public string ProviderChannelId { get; set; }
-    }
-
-    public class ChannelMappingOptions
-    {
-        public List<TunerChannelMapping> TunerChannels { get; set; }
-
-        public List<NameIdPair> ProviderChannels { get; set; }
-
-        public NameValuePair[] Mappings { get; set; }
-
-        public string ProviderName { get; set; }
-    }
-
-    [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
-    public class GetLiveStreamFile
-    {
-        public string Id { get; set; }
-
-        public string Container { get; set; }
-    }
-
-    [Route("/LiveTv/LiveRecordings/{Id}/stream", "GET", Summary = "Gets a live tv channel")]
-    public class GetLiveRecordingFile
-    {
-        public string Id { get; set; }
-    }
-
-    [Route("/LiveTv/TunerHosts/Types", "GET")]
-    [Authenticated]
-    public class GetTunerHostTypes : IReturn<List<NameIdPair>>
-    {
-    }
-
-    [Route("/LiveTv/Tuners/Discvover", "GET")]
-    [Authenticated]
-    public class DiscoverTuners : IReturn<List<TunerHostInfo>>
-    {
-        public bool NewDevicesOnly { get; set; }
-    }
-
-    public class LiveTvService : BaseApiService
-    {
-        private readonly ILiveTvManager _liveTvManager;
-        private readonly IUserManager _userManager;
-        private readonly IHttpClient _httpClient;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly ISessionContext _sessionContext;
-        private readonly IStreamHelper _streamHelper;
-        private readonly IMediaSourceManager _mediaSourceManager;
-
-        public LiveTvService(
-            ILogger<LiveTvService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IMediaSourceManager mediaSourceManager,
-            IStreamHelper streamHelper,
-            ILiveTvManager liveTvManager,
-            IUserManager userManager,
-            IHttpClient httpClient,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            ISessionContext sessionContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _mediaSourceManager = mediaSourceManager;
-            _streamHelper = streamHelper;
-            _liveTvManager = liveTvManager;
-            _userManager = userManager;
-            _httpClient = httpClient;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _sessionContext = sessionContext;
-        }
-
-        public object Get(GetTunerHostTypes request)
-        {
-            var list = _liveTvManager.GetTunerHostTypes();
-            return ToOptimizedResult(list);
-        }
-
-        public object Get(GetRecordingFolders request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-            var folders = _liveTvManager.GetRecordingFolders(user);
-
-            var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnArray,
-                TotalRecordCount = returnArray.Count
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetLiveRecordingFile request)
-        {
-            var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
-
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                throw new FileNotFoundException();
-            }
-
-            var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path)
-            };
-
-            return new ProgressiveFileCopier(_streamHelper, path, outputHeaders, Logger)
-            {
-                AllowEndOfFile = false
-            };
-        }
-
-        public async Task<object> Get(DiscoverTuners request)
-        {
-            var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetLiveStreamFile request)
-        {
-            var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            var directStreamProvider = liveStreamInfo;
-
-            var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-            {
-                [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container)
-            };
-
-            return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger)
-            {
-                AllowEndOfFile = false
-            };
-        }
-
-        public object Get(GetDefaultListingProvider request)
-        {
-            return ToOptimizedResult(new ListingsProviderInfo());
-        }
-
-        public async Task<object> Post(SetChannelMapping request)
-        {
-            return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelId, request.ProviderChannelId).ConfigureAwait(false);
-        }
-
-        public async Task<object> Get(GetChannelMappingOptions request)
-        {
-            var config = GetConfiguration();
-
-            var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(request.ProviderId, i.Id, StringComparison.OrdinalIgnoreCase));
-
-            var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
-
-            var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(request.ProviderId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-            var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(request.ProviderId, CancellationToken.None)
-                     .ConfigureAwait(false);
-
-            var mappings = listingsProviderInfo.ChannelMappings;
-
-            var result = new ChannelMappingOptions
-            {
-                TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
-
-                ProviderChannels = providerChannels.Select(i => new NameIdPair
-                {
-                    Name = i.Name,
-                    Id = i.Id
-                }).ToList(),
-
-                Mappings = mappings,
-
-                ProviderName = listingsProviderName
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetSchedulesDirectCountries request)
-        {
-            // https://json.schedulesdirect.org/20141201/available/countries
-
-            var response = await _httpClient.Get(new HttpRequestOptions
-            {
-                Url = "https://json.schedulesdirect.org/20141201/available/countries",
-                BufferContent = false
-            }).ConfigureAwait(false);
-
-            return ResultFactory.GetResult(Request, response, "application/json");
-        }
-
-        private void AssertUserCanManageLiveTv()
-        {
-            var user = _sessionContext.GetUser(Request);
-
-            if (user == null)
-            {
-                throw new SecurityException("Anonymous live tv management is not allowed.");
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
-            {
-                throw new SecurityException("The current user does not have permission to manage live tv.");
-            }
-        }
-
-        public async Task<object> Post(AddListingProvider request)
-        {
-            if (request.Pw != null)
-            {
-                request.Password = GetHashedString(request.Pw);
-            }
-
-            request.Pw = null;
-
-            var result = await _liveTvManager.SaveListingProvider(request, request.ValidateLogin, request.ValidateListings).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the hashed string.
-        /// </summary>
-        private string GetHashedString(string str)
-        {
-            // SchedulesDirect requires a SHA1 hash of the user's password
-            // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
-            using SHA1 sha = SHA1.Create();
-
-            return Hex.Encode(
-                sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
-        }
-
-        public void Delete(DeleteListingProvider request)
-        {
-            _liveTvManager.DeleteListingsProvider(request.Id);
-        }
-
-        public async Task<object> Post(AddTunerHost request)
-        {
-            var result = await _liveTvManager.SaveTunerHost(request).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        public void Delete(DeleteTunerHost request)
-        {
-            var config = GetConfiguration();
-
-            config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
-
-            ServerConfigurationManager.SaveConfiguration("livetv", config);
-        }
-
-        private LiveTvOptions GetConfiguration()
-        {
-            return ServerConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
-        }
-
-        private void UpdateConfiguration(LiveTvOptions options)
-        {
-            ServerConfigurationManager.SaveConfiguration("livetv", options);
-        }
-
-        public async Task<object> Get(GetLineups request)
-        {
-            var info = await _liveTvManager.GetLineups(request.Type, request.Id, request.Country, request.Location).ConfigureAwait(false);
-
-            return ToOptimizedResult(info);
-        }
-
-        public object Get(GetLiveTvInfo request)
-        {
-            var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None);
-
-            return ToOptimizedResult(info);
-        }
-
-        public object Get(GetChannels request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var channelResult = _liveTvManager.GetInternalChannels(new LiveTvChannelQuery
-            {
-                ChannelType = request.Type,
-                UserId = request.UserId,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                IsFavorite = request.IsFavorite,
-                IsLiked = request.IsLiked,
-                IsDisliked = request.IsDisliked,
-                EnableFavoriteSorting = request.EnableFavoriteSorting,
-                IsMovie = request.IsMovie,
-                IsSeries = request.IsSeries,
-                IsNews = request.IsNews,
-                IsKids = request.IsKids,
-                IsSports = request.IsSports,
-                SortBy = request.GetOrderBy(),
-                SortOrder = request.SortOrder ?? SortOrder.Ascending,
-                AddCurrentProgram = request.AddCurrentProgram
-            }, options, CancellationToken.None);
-
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            RemoveFields(options);
-
-            options.AddCurrentProgram = request.AddCurrentProgram;
-
-            var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnArray,
-                TotalRecordCount = channelResult.TotalRecordCount
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        private void RemoveFields(DtoOptions options)
-        {
-            var fields = options.Fields.ToList();
-
-            fields.Remove(ItemFields.CanDelete);
-            fields.Remove(ItemFields.CanDownload);
-            fields.Remove(ItemFields.DisplayPreferencesId);
-            fields.Remove(ItemFields.Etag);
-            options.Fields = fields.ToArray();
-        }
-
-        public object Get(GetChannel request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetPrograms request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                ChannelIds = ApiEntryPoint.Split(request.ChannelIds, ',', true).Select(i => new Guid(i)).ToArray(),
-                HasAired = request.HasAired,
-                IsAiring = request.IsAiring,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            if (!string.IsNullOrEmpty(request.MinStartDate))
-            {
-                query.MinStartDate = DateTime.Parse(request.MinStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinEndDate))
-            {
-                query.MinEndDate = DateTime.Parse(request.MinEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxStartDate))
-            {
-                query.MaxStartDate = DateTime.Parse(request.MaxStartDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxEndDate))
-            {
-                query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            query.StartIndex = request.StartIndex;
-            query.Limit = request.Limit;
-            query.OrderBy = BaseItemsRequest.GetOrderBy(request.SortBy, request.SortOrder);
-            query.IsNews = request.IsNews;
-            query.IsMovie = request.IsMovie;
-            query.IsSeries = request.IsSeries;
-            query.IsKids = request.IsKids;
-            query.IsSports = request.IsSports;
-            query.SeriesTimerId = request.SeriesTimerId;
-            query.Genres = (request.Genres ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-            query.GenreIds = GetGuids(request.GenreIds);
-
-            if (!request.LibrarySeriesId.Equals(Guid.Empty))
-            {
-                query.IsSeries = true;
-
-                if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series)
-                {
-                    query.Name = series.Name;
-                }
-            }
-
-            var result = await _liveTvManager.GetPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetRecommendedPrograms request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IsAiring = request.IsAiring,
-                Limit = request.Limit,
-                HasAired = request.HasAired,
-                IsSeries = request.IsSeries,
-                IsMovie = request.IsMovie,
-                IsKids = request.IsKids,
-                IsNews = request.IsNews,
-                IsSports = request.IsSports,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            query.GenreIds = GetGuids(request.GenreIds);
-
-            var result = _liveTvManager.GetRecommendedPrograms(query, GetDtoOptions(_authContext, request), CancellationToken.None);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Post(GetPrograms request)
-        {
-            return Get(request);
-        }
-
-        public object Get(GetRecordings request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var result = _liveTvManager.GetRecordings(new RecordingQuery
-            {
-                ChannelId = request.ChannelId,
-                UserId = request.UserId,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                Status = request.Status,
-                SeriesTimerId = request.SeriesTimerId,
-                IsInProgress = request.IsInProgress,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                IsMovie = request.IsMovie,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries,
-                IsKids = request.IsKids,
-                IsSports = request.IsSports,
-                IsLibraryItem = request.IsLibraryItem,
-                Fields = request.GetItemFields(),
-                ImageTypeLimit = request.ImageTypeLimit,
-                EnableImages = request.EnableImages
-            }, options);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetRecordingSeries request)
-        {
-            return ToOptimizedResult(new QueryResult<BaseItemDto>());
-        }
-
-        public object Get(GetRecording request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetTimer request)
-        {
-            var result = await _liveTvManager.GetTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetTimers request)
-        {
-            var result = await _liveTvManager.GetTimers(new TimerQuery
-            {
-                ChannelId = request.ChannelId,
-                SeriesTimerId = request.SeriesTimerId,
-                IsActive = request.IsActive,
-                IsScheduled = request.IsScheduled
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Delete(DeleteRecording request)
-        {
-            AssertUserCanManageLiveTv();
-
-            _libraryManager.DeleteItem(_libraryManager.GetItemById(request.Id), new DeleteOptions
-            {
-                DeleteFileLocation = false
-            });
-        }
-
-        public Task Delete(CancelTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CancelTimer(request.Id);
-        }
-
-        public Task Post(UpdateTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.UpdateTimer(request, CancellationToken.None);
-        }
-
-        public async Task<object> Get(GetSeriesTimers request)
-        {
-            var result = await _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
-            {
-                SortOrder = request.SortOrder,
-                SortBy = request.SortBy
-            }, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetSeriesTimer request)
-        {
-            var result = await _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task Delete(CancelSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CancelSeriesTimer(request.Id);
-        }
-
-        public Task Post(UpdateSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.UpdateSeriesTimer(request, CancellationToken.None);
-        }
-
-        public async Task<object> Get(GetDefaultTimer request)
-        {
-            if (string.IsNullOrEmpty(request.ProgramId))
-            {
-                var result = await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-            else
-            {
-                var result = await _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-        }
-
-        public async Task<object> Get(GetProgram request)
-        {
-            var user = request.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(request.UserId);
-
-            var result = await _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task Post(CreateSeriesTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CreateSeriesTimer(request, CancellationToken.None);
-        }
-
-        public Task Post(CreateTimer request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.CreateTimer(request, CancellationToken.None);
-        }
-
-        public object Get(GetRecordingGroups request)
-        {
-            return ToOptimizedResult(new QueryResult<BaseItemDto>());
-        }
-
-        public object Get(GetRecordingGroup request)
-        {
-            throw new FileNotFoundException();
-        }
-
-        public object Get(GetGuideInfo request)
-        {
-            return ToOptimizedResult(_liveTvManager.GetGuideInfo());
-        }
-
-        public Task Post(ResetTuner request)
-        {
-            AssertUserCanManageLiveTv();
-
-            return _liveTvManager.ResetTuner(request.Id, CancellationToken.None);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs
deleted file mode 100644
index d6b5f51950..0000000000
--- a/MediaBrowser.Api/LocalizationService.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetCultures.
-    /// </summary>
-    [Route("/Localization/Cultures", "GET", Summary = "Gets known cultures")]
-    public class GetCultures : IReturn<CultureDto[]>
-    {
-    }
-
-    /// <summary>
-    /// Class GetCountries.
-    /// </summary>
-    [Route("/Localization/Countries", "GET", Summary = "Gets known countries")]
-    public class GetCountries : IReturn<CountryInfo[]>
-    {
-    }
-
-    /// <summary>
-    /// Class ParentalRatings.
-    /// </summary>
-    [Route("/Localization/ParentalRatings", "GET", Summary = "Gets known parental ratings")]
-    public class GetParentalRatings : IReturn<ParentalRating[]>
-    {
-    }
-
-    /// <summary>
-    /// Class ParentalRatings.
-    /// </summary>
-    [Route("/Localization/Options", "GET", Summary = "Gets localization options")]
-    public class GetLocalizationOptions : IReturn<LocalizationOption[]>
-    {
-    }
-
-    /// <summary>
-    /// Class CulturesService.
-    /// </summary>
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class LocalizationService : BaseApiService
-    {
-        /// <summary>
-        /// The _localization.
-        /// </summary>
-        private readonly ILocalizationManager _localization;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LocalizationService"/> class.
-        /// </summary>
-        /// <param name="localization">The localization.</param>
-        public LocalizationService(
-            ILogger<LocalizationService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILocalizationManager localization)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _localization = localization;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetParentalRatings request)
-        {
-            var result = _localization.GetParentalRatings();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetLocalizationOptions request)
-        {
-            var result = _localization.GetLocalizationOptions();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCountries request)
-        {
-            var result = _localization.GetCountries();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetCultures request)
-        {
-            var result = _localization.GetCultures();
-
-            return ToOptimizedResult(result);
-        }
-    }
-
-}
diff --git a/MediaBrowser.Api/Movies/CollectionService.cs b/MediaBrowser.Api/Movies/CollectionService.cs
deleted file mode 100644
index e9629439de..0000000000
--- a/MediaBrowser.Api/Movies/CollectionService.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Collections;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
-    [Route("/Collections", "POST", Summary = "Creates a new collection")]
-    public class CreateCollection : IReturn<CollectionCreationResult>
-    {
-        [ApiMember(Name = "IsLocked", Description = "Whether or not to lock the new collection.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool IsLocked { get; set; }
-
-        [ApiMember(Name = "Name", Description = "The name of the new collection.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Optional - create the collection within a specific folder", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "Ids", Description = "Item Ids to add to the collection", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
-    }
-
-    [Route("/Collections/{Id}/Items", "POST", Summary = "Adds items to a collection")]
-    public class AddToCollection : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Collections/{Id}/Items", "DELETE", Summary = "Removes items from a collection")]
-    public class RemoveFromCollection : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Authenticated]
-    public class CollectionService : BaseApiService
-    {
-        private readonly ICollectionManager _collectionManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        public CollectionService(
-            ILogger<CollectionService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ICollectionManager collectionManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _collectionManager = collectionManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Post(CreateCollection request)
-        {
-            var userId = _authContext.GetAuthorizationInfo(Request).UserId;
-
-            var parentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId);
-
-            var item = _collectionManager.CreateCollection(new CollectionCreationOptions
-            {
-                IsLocked = request.IsLocked,
-                Name = request.Name,
-                ParentId = parentId,
-                ItemIdList = SplitValue(request.Ids, ','),
-                UserIds = new[] { userId }
-            });
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
-
-            return new CollectionCreationResult
-            {
-                Id = dto.Id
-            };
-        }
-
-        public void Post(AddToCollection request)
-        {
-            _collectionManager.AddToCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
-        }
-
-        public void Delete(RemoveFromCollection request)
-        {
-            _collectionManager.RemoveFromCollection(new Guid(request.Id), SplitValue(request.Ids, ','));
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
deleted file mode 100644
index 34cccffa38..0000000000
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ /dev/null
@@ -1,414 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-
-namespace MediaBrowser.Api.Movies
-{
-    [Route("/Movies/Recommendations", "GET", Summary = "Gets movie recommendations")]
-    public class GetMovieRecommendations : IReturn<RecommendationDto[]>, IHasDtoOptions
-    {
-        [ApiMember(Name = "CategoryLimit", Description = "The max number of categories to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int CategoryLimit { get; set; }
-
-        [ApiMember(Name = "ItemLimit", Description = "The max number of items to return per category", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int ItemLimit { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        public GetMovieRecommendations()
-        {
-            CategoryLimit = 5;
-            ItemLimit = 8;
-        }
-
-        public string Fields { get; set; }
-    }
-
-    /// <summary>
-    /// Class MoviesService.
-    /// </summary>
-    [Authenticated]
-    public class MoviesService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        private readonly ILibraryManager _libraryManager;
-
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MoviesService" /> class.
-        /// </summary>
-        public MoviesService(
-            ILogger<MoviesService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Get(GetMovieRecommendations request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = GetRecommendationCategories(user, request.ParentId, request.CategoryLimit, request.ItemLimit, dtoOptions);
-
-            return ToOptimizedResult(result);
-        }
-
-        public QueryResult<BaseItemDto> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
-                _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
-
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                Limit = request.Limit,
-                IncludeItemTypes = itemTypes.ToArray(),
-                IsMovie = true,
-                SimilarTo = item,
-                EnableGroupByMetadataKey = true,
-                DtoOptions = dtoOptions
-
-            });
-
-            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnList,
-
-                TotalRecordCount = itemsResult.Count
-            };
-
-            return result;
-        }
-
-        private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions)
-        {
-            var categories = new List<RecommendationDto>();
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[]
-                {
-                    typeof(Movie).Name,
-                    // typeof(Trailer).Name,
-                    // typeof(LiveTvProgram).Name
-                },
-                // IsMovie = true
-                OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                Limit = 7,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                IsPlayed = true,
-                DtoOptions = dtoOptions
-            };
-
-            var recentlyPlayedMovies = _libraryManager.GetItemList(query);
-
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = itemTypes.ToArray(),
-                IsMovie = true,
-                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                Limit = 10,
-                IsFavoriteOrLiked = true,
-                ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
-                EnableGroupByMetadataKey = true,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = dtoOptions
-            });
-
-            var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
-            // Get recently played directors
-            var recentDirectors = GetDirectors(mostRecentMovies)
-                .ToList();
-
-            // Get recently played actors
-            var recentActors = GetActors(mostRecentMovies)
-                .ToList();
-
-            var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
-            var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
-
-            var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
-            var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
-
-            var categoryTypes = new List<IEnumerator<RecommendationDto>>
-            {
-                // Give this extra weight
-                similarToRecentlyPlayed,
-                similarToRecentlyPlayed,
-
-                // Give this extra weight
-                similarToLiked,
-                similarToLiked,
-
-                hasDirectorFromRecentlyPlayed,
-                hasActorFromRecentlyPlayed
-            };
-
-            while (categories.Count < categoryLimit)
-            {
-                var allEmpty = true;
-
-                foreach (var category in categoryTypes)
-                {
-                    if (category.MoveNext())
-                    {
-                        categories.Add(category.Current);
-                        allEmpty = false;
-
-                        if (categories.Count >= categoryLimit)
-                        {
-                            break;
-                        }
-                    }
-                }
-
-                if (allEmpty)
-                {
-                    break;
-                }
-            }
-
-            return categories.OrderBy(i => i.RecommendationType);
-        }
-
-        private IEnumerable<RecommendationDto> GetWithDirector(
-            User user,
-            IEnumerable<string> names,
-            int itemLimit,
-            DtoOptions dtoOptions,
-            RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var name in names)
-            {
-                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Person = name,
-                    // Account for duplicates by imdb id, since the database doesn't support this yet
-                    Limit = itemLimit + 2,
-                    PersonTypes = new[] { PersonType.Director },
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-                }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                .Select(x => x.First())
-                .Take(itemLimit)
-                .ToList();
-
-                if (items.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = name,
-                        CategoryId = name.GetMD5(),
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var name in names)
-            {
-                var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Person = name,
-                    // Account for duplicates by imdb id, since the database doesn't support this yet
-                    Limit = itemLimit + 2,
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-                }).GroupBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
-                .Select(x => x.First())
-                .Take(itemLimit)
-                .ToList();
-
-                if (items.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = name,
-                        CategoryId = name.GetMD5(),
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
-        {
-            var itemTypes = new List<string> { typeof(Movie).Name };
-            if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-            {
-                itemTypes.Add(typeof(Trailer).Name);
-                itemTypes.Add(typeof(LiveTvProgram).Name);
-            }
-
-            foreach (var item in baselineItems)
-            {
-                var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                {
-                    Limit = itemLimit,
-                    IncludeItemTypes = itemTypes.ToArray(),
-                    IsMovie = true,
-                    SimilarTo = item,
-                    EnableGroupByMetadataKey = true,
-                    DtoOptions = dtoOptions
-                });
-
-                if (similar.Count > 0)
-                {
-                    var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
-                    yield return new RecommendationDto
-                    {
-                        BaselineItemName = item.Name,
-                        CategoryId = item.Id,
-                        RecommendationType = type,
-                        Items = returnItems
-                    };
-                }
-            }
-        }
-
-        private IEnumerable<string> GetActors(List<BaseItem> items)
-        {
-            var people = _libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                ExcludePersonTypes = new[]
-                {
-                    PersonType.Director
-                },
-                MaxListOrder = 3
-            });
-
-            var itemIds = items.Select(i => i.Id).ToList();
-
-            return people
-                .Where(i => itemIds.Contains(i.ItemId))
-                .Select(i => i.Name)
-                .DistinctNames();
-        }
-
-        private IEnumerable<string> GetDirectors(List<BaseItem> items)
-        {
-            var people = _libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                PersonTypes = new[]
-                {
-                    PersonType.Director
-                }
-            });
-
-            var itemIds = items.Select(i => i.Id).ToList();
-
-            return people
-                .Where(i => itemIds.Contains(i.ItemId))
-                .Select(i => i.Name)
-                .DistinctNames();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs
deleted file mode 100644
index ca9f9d03b2..0000000000
--- a/MediaBrowser.Api/Movies/TrailersService.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using MediaBrowser.Api.UserLibrary;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Movies
-{
-    [Route("/Trailers", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
-    public class Getrailers : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    /// <summary>
-    /// Class TrailersService.
-    /// </summary>
-    [Authenticated]
-    public class TrailersService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// The logger for the created <see cref="ItemsService"/> instances.
-        /// </summary>
-        private readonly ILogger<ItemsService> _logger;
-
-        private readonly IDtoService _dtoService;
-        private readonly ILocalizationManager _localizationManager;
-        private readonly IJsonSerializer _json;
-        private readonly IAuthorizationContext _authContext;
-
-        public TrailersService(
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            ILocalizationManager localizationManager,
-            IJsonSerializer json,
-            IAuthorizationContext authContext)
-            : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _localizationManager = localizationManager;
-            _json = json;
-            _authContext = authContext;
-            _logger = loggerFactory.CreateLogger<ItemsService>();
-        }
-
-        public object Get(Getrailers request)
-        {
-            var json = _json.SerializeToString(request);
-            var getItems = _json.DeserializeFromString<GetItems>(json);
-
-            getItems.IncludeItemTypes = "Trailer";
-
-            return new ItemsService(
-                _logger,
-                ServerConfigurationManager,
-                ResultFactory,
-                _userManager,
-                _libraryManager,
-                _localizationManager,
-                _dtoService,
-                _authContext)
-            {
-                Request = Request,
-            }.Get(getItems);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs
deleted file mode 100644
index 74d3cce12a..0000000000
--- a/MediaBrowser.Api/Music/AlbumsService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
-    [Route("/Albums/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    public class GetSimilarAlbums : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Artists/{Id}/Similar", "GET", Summary = "Finds albums similar to a given album.")]
-    public class GetSimilarArtists : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Authenticated]
-    public class AlbumsService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _user data repository.
-        /// </summary>
-        private readonly IUserDataManager _userDataRepository;
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly IItemRepository _itemRepo;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        public AlbumsService(
-            ILogger<AlbumsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserDataManager userDataRepository,
-            ILibraryManager libraryManager,
-            IItemRepository itemRepo,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userDataRepository = userDataRepository;
-            _libraryManager = libraryManager;
-            _itemRepo = itemRepo;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Get(GetSimilarArtists request)
-        {
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions, 
-                _userManager,
-                _itemRepo,
-                _libraryManager,
-                _userDataRepository,
-                _dtoService,
-                request, new[] { typeof(MusicArtist) },
-                SimilarItemsHelper.GetSimiliarityScore);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSimilarAlbums request)
-        {
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions, 
-                _userManager,
-                _itemRepo,
-                _libraryManager,
-                _userDataRepository,
-                _dtoService,
-                request, new[] { typeof(MusicAlbum) },
-                GetAlbumSimilarityScore);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the album similarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item1People">The item1 people.</param>
-        /// <param name="allPeople">All people.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
-        {
-            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
-
-            var album1 = (MusicAlbum)item1;
-            var album2 = (MusicAlbum)item2;
-
-            var artists1 = album1
-                .GetAllArtists()
-                .DistinctNames()
-                .ToList();
-
-            var artists2 = new HashSet<string>(
-                album2.GetAllArtists().DistinctNames(),
-                StringComparer.OrdinalIgnoreCase);
-
-            return points + artists1.Where(artists2.Contains).Sum(i => 5);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs
deleted file mode 100644
index ebd3eb64a1..0000000000
--- a/MediaBrowser.Api/Music/InstantMixService.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Music
-{
-    [Route("/Songs/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given song")]
-    public class GetInstantMixFromSong : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Albums/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given album")]
-    public class GetInstantMixFromAlbum : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/Playlists/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given playlist")]
-    public class GetInstantMixFromPlaylist : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Route("/MusicGenres/{Name}/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
-    public class GetInstantMixFromMusicGenre : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    [Route("/Artists/InstantMix", "GET", Summary = "Creates an instant playlist based on a given artist")]
-    public class GetInstantMixFromArtistId : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Id", Description = "The artist Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
-    public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
-    {
-        [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Items/{Id}/InstantMix", "GET", Summary = "Creates an instant playlist based on a given item")]
-    public class GetInstantMixFromItem : BaseGetSimilarItemsFromItem
-    {
-    }
-
-    [Authenticated]
-    public class InstantMixService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-
-        private readonly IDtoService _dtoService;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMusicManager _musicManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public InstantMixService(
-            ILogger<InstantMixService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IDtoService dtoService,
-            IMusicManager musicManager,
-            ILibraryManager libraryManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _dtoService = dtoService;
-            _musicManager = musicManager;
-            _libraryManager = libraryManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetInstantMixFromItem request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromArtistId request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromMusicGenreId request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromSong request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromAlbum request)
-        {
-            var album = _libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromPlaylist request)
-        {
-            var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        public object Get(GetInstantMixFromMusicGenre request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user, dtoOptions);
-
-            return GetResult(items, user, request, dtoOptions);
-        }
-
-        private object GetResult(List<BaseItem> items, User user, BaseGetSimilarItems request, DtoOptions dtoOptions)
-        {
-            var list = items;
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = list.Count
-            };
-
-            if (request.Limit.HasValue)
-            {
-                list = list.Take(request.Limit.Value).ToList();
-            }
-
-            var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
-            result.Items = returnList;
-
-            return result;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs
deleted file mode 100644
index a84556fcc4..0000000000
--- a/MediaBrowser.Api/PackageService.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Repositories", "GET", Summary = "Gets all package repositories")]
-    [Authenticated]
-    public class GetRepositories : IReturnVoid
-    {
-    }
-
-    [Route("/Repositories", "POST", Summary = "Sets the enabled and existing package repositories")]
-    [Authenticated]
-    public class SetRepositories : List<RepositoryInfo>, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class GetPackage.
-    /// </summary>
-    [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")]
-    [Authenticated]
-    public class GetPackage : IReturn<PackageInfo>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AssemblyGuid { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPackages.
-    /// </summary>
-    [Route("/Packages", "GET", Summary = "Gets available packages")]
-    [Authenticated]
-    public class GetPackages : IReturn<PackageInfo[]>
-    {
-    }
-
-    /// <summary>
-    /// Class InstallPackage.
-    /// </summary>
-    [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")]
-    [Authenticated(Roles = "Admin")]
-    public class InstallPackage : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string AssemblyGuid { get; set; }
-
-        /// <summary>
-        /// Gets or sets the version.
-        /// </summary>
-        /// <value>The version.</value>
-        [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Version { get; set; }
-    }
-
-    /// <summary>
-    /// Class CancelPackageInstallation.
-    /// </summary>
-    [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")]
-    [Authenticated(Roles = "Admin")]
-    public class CancelPackageInstallation : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class PackageService.
-    /// </summary>
-    public class PackageService : BaseApiService
-    {
-        private readonly IInstallationManager _installationManager;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-
-        public PackageService(
-            ILogger<PackageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IInstallationManager installationManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _installationManager = installationManager;
-            _serverConfigurationManager = serverConfigurationManager;
-        }
-
-        public object Get(GetRepositories request)
-        {
-            var result = _serverConfigurationManager.Configuration.PluginRepositories;
-            return ToOptimizedResult(result);
-        }
-
-        public void Post(SetRepositories request)
-        {
-            _serverConfigurationManager.Configuration.PluginRepositories = request;
-            _serverConfigurationManager.SaveConfiguration();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPackage request)
-        {
-            var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult();
-            var result = _installationManager.FilterPackages(
-                packages,
-                request.Name,
-                string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetPackages request)
-        {
-            IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
-
-            return ToOptimizedResult(packages.ToArray());
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException"></exception>
-        public async Task Post(InstallPackage request)
-        {
-            var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
-            var package = _installationManager.GetCompatibleVersions(
-                    packages,
-                    request.Name,
-                    string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid),
-                    string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault();
-
-            if (package == null)
-            {
-                throw new ResourceNotFoundException(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "Package not found: {0}",
-                        request.Name));
-            }
-
-            await _installationManager.InstallPackage(package);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(CancelPackageInstallation request)
-        {
-            _installationManager.CancelInstallation(new Guid(request.Id));
-        }
-    }
-}
diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs
deleted file mode 100644
index 5513c08922..0000000000
--- a/MediaBrowser.Api/PlaylistService.cs
+++ /dev/null
@@ -1,216 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Playlists;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Playlists", "POST", Summary = "Creates a new playlist")]
-    public class CreatePlaylist : IReturn<PlaylistCreationResult>
-    {
-        [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaType { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")]
-    public class AddToPlaylist : IReturnVoid
-    {
-        [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Ids { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public Guid UserId { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")]
-    public class MoveItem : IReturnVoid
-    {
-        [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public int NewIndex { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
-    public class RemoveFromPlaylist : IReturnVoid
-    {
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string EntryIds { get; set; }
-    }
-
-    [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
-    public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-    }
-
-    [Authenticated]
-    public class PlaylistService : BaseApiService
-    {
-        private readonly IPlaylistManager _playlistManager;
-        private readonly IDtoService _dtoService;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public PlaylistService(
-            ILogger<PlaylistService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDtoService dtoService,
-            IPlaylistManager playlistManager,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _dtoService = dtoService;
-            _playlistManager = playlistManager;
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _authContext = authContext;
-        }
-
-        public void Post(MoveItem request)
-        {
-            _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex);
-        }
-
-        public async Task<object> Post(CreatePlaylist request)
-        {
-            var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
-            {
-                Name = request.Name,
-                ItemIdList = GetGuids(request.Ids),
-                UserId = request.UserId,
-                MediaType = request.MediaType
-            }).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public void Post(AddToPlaylist request)
-        {
-            _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId);
-        }
-
-        public void Delete(RemoveFromPlaylist request)
-        {
-            _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
-        }
-
-        public object Get(GetPlaylistItems request)
-        {
-            var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var items = playlist.GetManageableItems().ToArray();
-
-            var count = items.Length;
-
-            if (request.StartIndex.HasValue)
-            {
-                items = items.Skip(request.StartIndex.Value).ToArray();
-            }
-
-            if (request.Limit.HasValue)
-            {
-                items = items.Take(request.Limit.Value).ToArray();
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
-
-            for (int index = 0; index < dtos.Count; index++)
-            {
-                dtos[index].PlaylistItemId = items[index].Item1.Id;
-            }
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = count
-            };
-
-            return ToOptimizedResult(result);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs
deleted file mode 100644
index 7d976ceaac..0000000000
--- a/MediaBrowser.Api/PluginService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class Plugins.
-    /// </summary>
-    [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")]
-    [Authenticated]
-    public class GetPlugins : IReturn<PluginInfo[]>
-    {
-        public bool? IsAppStoreEnabled { get; set; }
-    }
-
-    /// <summary>
-    /// Class UninstallPlugin.
-    /// </summary>
-    [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")]
-    [Authenticated(Roles = "Admin")]
-    public class UninstallPlugin : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPluginConfiguration.
-    /// </summary>
-    [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")]
-    [Authenticated]
-    public class GetPluginConfiguration
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdatePluginConfiguration.
-    /// </summary>
-    [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")]
-    [Authenticated]
-    public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The raw Http Request Input Stream.
-        /// </summary>
-        /// <value>The request stream.</value>
-        public Stream RequestStream { get; set; }
-    }
-
-    // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
-    // delete all these registration endpoints. They are only kept for compatibility.
-    [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
-    [Authenticated]
-    public class GetRegistration : IReturn<RegistrationInfo>
-    {
-        [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPluginSecurityInfo.
-    /// </summary>
-    [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)]
-    [Authenticated]
-    public class GetPluginSecurityInfo : IReturn<PluginSecurityInfo>
-    {
-    }
-
-    /// <summary>
-    /// Class UpdatePluginSecurityInfo.
-    /// </summary>
-    [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)]
-    [Authenticated(Roles = "Admin")]
-    public class UpdatePluginSecurityInfo : PluginSecurityInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Plugins/RegistrationRecords/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
-    [Authenticated]
-    public class GetRegistrationStatus
-    {
-        [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    // TODO these two classes are only kept for compability with paid plugins and should be removed
-    public class RegistrationInfo
-    {
-        public string Name { get; set; }
-
-        public DateTime ExpirationDate { get; set; }
-
-        public bool IsTrial { get; set; }
-
-        public bool IsRegistered { get; set; }
-    }
-
-    public class MBRegistrationRecord
-    {
-        public DateTime ExpirationDate { get; set; }
-
-        public bool IsRegistered { get; set; }
-
-        public bool RegChecked { get; set; }
-
-        public bool RegError { get; set; }
-
-        public bool TrialVersion { get; set; }
-
-        public bool IsValid { get; set; }
-    }
-
-    public class PluginSecurityInfo
-    {
-        public string SupporterKey { get; set; }
-
-        public bool IsMBSupporter { get; set; }
-    }
-    /// <summary>
-    /// Class PluginsService.
-    /// </summary>
-    public class PluginService : BaseApiService
-    {
-        /// <summary>
-        /// The _json serializer.
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        /// <summary>
-        /// The _app host.
-        /// </summary>
-        private readonly IApplicationHost _appHost;
-        private readonly IInstallationManager _installationManager;
-
-        public PluginService(
-            ILogger<PluginService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IJsonSerializer jsonSerializer,
-            IApplicationHost appHost,
-            IInstallationManager installationManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appHost = appHost;
-            _installationManager = installationManager;
-            _jsonSerializer = jsonSerializer;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRegistrationStatus request)
-        {
-            var record = new MBRegistrationRecord
-            {
-                IsRegistered = true,
-                RegChecked = true,
-                TrialVersion = false,
-                IsValid = true,
-                RegError = false
-            };
-
-            return ToOptimizedResult(record);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPlugins request)
-        {
-            var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToArray();
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPluginConfiguration request)
-        {
-            var guid = new Guid(request.Id);
-            var plugin = _appHost.Plugins.First(p => p.Id == guid) as IHasPluginConfiguration;
-
-            return ToOptimizedResult(plugin.Configuration);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPluginSecurityInfo request)
-        {
-            var result = new PluginSecurityInfo
-            {
-                IsMBSupporter = true,
-                SupporterKey = "IAmTotallyLegit"
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(UpdatePluginSecurityInfo request)
-        {
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(UpdatePluginConfiguration request)
-        {
-            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
-            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var id = Guid.Parse(GetPathValue(1));
-
-            if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin))
-            {
-                throw new FileNotFoundException();
-            }
-
-            var configuration = (await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, plugin.ConfigurationType).ConfigureAwait(false)) as BasePluginConfiguration;
-
-            plugin.UpdateConfiguration(configuration);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Delete(UninstallPlugin request)
-        {
-            var guid = new Guid(request.Id);
-            var plugin = _appHost.Plugins.First(p => p.Id == guid);
-
-            _installationManager.UninstallPlugin(plugin);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
deleted file mode 100644
index 86b00316ad..0000000000
--- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.ScheduledTasks
-{
-    /// <summary>
-    /// Class GetScheduledTask.
-    /// </summary>
-    [Route("/ScheduledTasks/{Id}", "GET", Summary = "Gets a scheduled task, by Id")]
-    public class GetScheduledTask : IReturn<TaskInfo>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetScheduledTasks.
-    /// </summary>
-    [Route("/ScheduledTasks", "GET", Summary = "Gets scheduled tasks")]
-    public class GetScheduledTasks : IReturn<TaskInfo[]>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional filter tasks that are hidden, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-
-        [ApiMember(Name = "IsEnabled", Description = "Optional filter tasks that are enabled, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsEnabled { get; set; }
-    }
-
-    /// <summary>
-    /// Class StartScheduledTask.
-    /// </summary>
-    [Route("/ScheduledTasks/Running/{Id}", "POST", Summary = "Starts a scheduled task")]
-    public class StartScheduledTask : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class StopScheduledTask.
-    /// </summary>
-    [Route("/ScheduledTasks/Running/{Id}", "DELETE", Summary = "Stops a scheduled task")]
-    public class StopScheduledTask : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateScheduledTaskTriggers.
-    /// </summary>
-    [Route("/ScheduledTasks/{Id}/Triggers", "POST", Summary = "Updates the triggers for a scheduled task")]
-    public class UpdateScheduledTaskTriggers : List<TaskTriggerInfo>, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the task id.
-        /// </summary>
-        /// <value>The task id.</value>
-        [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class ScheduledTasksService.
-    /// </summary>
-    [Authenticated(Roles = "Admin")]
-    public class ScheduledTaskService : BaseApiService
-    {
-        /// <summary>
-        /// The task manager.
-        /// </summary>
-        private readonly ITaskManager _taskManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScheduledTaskService" /> class.
-        /// </summary>
-        /// <param name="taskManager">The task manager.</param>
-        /// <exception cref="ArgumentNullException">taskManager</exception>
-        public ScheduledTaskService(
-            ILogger<ScheduledTaskService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ITaskManager taskManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _taskManager = taskManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{TaskInfo}.</returns>
-        public object Get(GetScheduledTasks request)
-        {
-            IEnumerable<IScheduledTaskWorker> result = _taskManager.ScheduledTasks
-                .OrderBy(i => i.Name);
-
-            if (request.IsHidden.HasValue)
-            {
-                var val = request.IsHidden.Value;
-
-                result = result.Where(i =>
-                {
-                    var isHidden = false;
-
-                    if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
-                    {
-                        isHidden = configurableTask.IsHidden;
-                    }
-
-                    return isHidden == val;
-                });
-            }
-
-            if (request.IsEnabled.HasValue)
-            {
-                var val = request.IsEnabled.Value;
-
-                result = result.Where(i =>
-                {
-                    var isEnabled = true;
-
-                    if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
-                    {
-                        isEnabled = configurableTask.IsEnabled;
-                    }
-
-                    return isEnabled == val;
-                });
-            }
-
-            var infos = result
-                .Select(ScheduledTaskHelpers.GetTaskInfo)
-                .ToArray();
-
-            return ToOptimizedResult(infos);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{TaskInfo}.</returns>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public object Get(GetScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            var result = ScheduledTaskHelpers.GetTaskInfo(task);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Post(StartScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            _taskManager.Execute(task, new TaskOptions());
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Delete(StopScheduledTask request)
-        {
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, request.Id));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            _taskManager.Cancel(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <exception cref="ResourceNotFoundException">Task not found</exception>
-        public void Post(UpdateScheduledTaskTriggers request)
-        {
-            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
-            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
-            var id = GetPathValue(1).ToString();
-
-            var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.Ordinal));
-
-            if (task == null)
-            {
-                throw new ResourceNotFoundException("Task not found");
-            }
-
-            task.Triggers = request.ToArray();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs
deleted file mode 100644
index 64ee69300e..0000000000
--- a/MediaBrowser.Api/SearchService.cs
+++ /dev/null
@@ -1,332 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Search;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetSearchHints.
-    /// </summary>
-    [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")]
-    public class GetSearchHints : IReturn<SearchHintResult>
-    {
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Supply a user id to search within a user's library or omit to search all.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Search characters used to find items.
-        /// </summary>
-        /// <value>The index by.</value>
-        [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SearchTerm { get; set; }
-
-
-        [ApiMember(Name = "IncludePeople", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludePeople { get; set; }
-
-        [ApiMember(Name = "IncludeMedia", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeMedia { get; set; }
-
-        [ApiMember(Name = "IncludeGenres", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeGenres { get; set; }
-
-        [ApiMember(Name = "IncludeStudios", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeStudios { get; set; }
-
-        [ApiMember(Name = "IncludeArtists", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool IncludeArtists { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemTypes { get; set; }
-
-        [ApiMember(Name = "MediaTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsMovie { get; set; }
-
-        [ApiMember(Name = "IsSeries", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSeries { get; set; }
-
-        [ApiMember(Name = "IsNews", Description = "Optional filter for news.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsNews { get; set; }
-
-        [ApiMember(Name = "IsKids", Description = "Optional filter for kids.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsKids { get; set; }
-
-        [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
-        public bool? IsSports { get; set; }
-
-        public GetSearchHints()
-        {
-            IncludeArtists = true;
-            IncludeGenres = true;
-            IncludeMedia = true;
-            IncludePeople = true;
-            IncludeStudios = true;
-        }
-    }
-
-    /// <summary>
-    /// Class SearchService.
-    /// </summary>
-    [Authenticated]
-    public class SearchService : BaseApiService
-    {
-        /// <summary>
-        /// The _search engine.
-        /// </summary>
-        private readonly ISearchEngine _searchEngine;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IImageProcessor _imageProcessor;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SearchService" /> class.
-        /// </summary>
-        /// <param name="searchEngine">The search engine.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="dtoService">The dto service.</param>
-        /// <param name="imageProcessor">The image processor.</param>
-        public SearchService(
-            ILogger<SearchService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISearchEngine searchEngine,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            IImageProcessor imageProcessor)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _searchEngine = searchEngine;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _imageProcessor = imageProcessor;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSearchHints request)
-        {
-            var result = GetSearchHintsAsync(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the search hints async.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{IEnumerable{SearchHintResult}}.</returns>
-        private SearchHintResult GetSearchHintsAsync(GetSearchHints request)
-        {
-            var result = _searchEngine.GetSearchHints(new SearchQuery
-            {
-                Limit = request.Limit,
-                SearchTerm = request.SearchTerm,
-                IncludeArtists = request.IncludeArtists,
-                IncludeGenres = request.IncludeGenres,
-                IncludeMedia = request.IncludeMedia,
-                IncludePeople = request.IncludePeople,
-                IncludeStudios = request.IncludeStudios,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
-                ExcludeItemTypes = ApiEntryPoint.Split(request.ExcludeItemTypes, ',', true),
-                MediaTypes = ApiEntryPoint.Split(request.MediaTypes, ',', true),
-                ParentId = request.ParentId,
-
-                IsKids = request.IsKids,
-                IsMovie = request.IsMovie,
-                IsNews = request.IsNews,
-                IsSeries = request.IsSeries,
-                IsSports = request.IsSports
-            });
-
-            return new SearchHintResult
-            {
-                TotalRecordCount = result.TotalRecordCount,
-
-                SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
-            };
-        }
-
-        /// <summary>
-        /// Gets the search hint result.
-        /// </summary>
-        /// <param name="hintInfo">The hint info.</param>
-        /// <returns>SearchHintResult.</returns>
-        private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
-        {
-            var item = hintInfo.Item;
-
-            var result = new SearchHint
-            {
-                Name = item.Name,
-                IndexNumber = item.IndexNumber,
-                ParentIndexNumber = item.ParentIndexNumber,
-                Id = item.Id,
-                Type = item.GetClientTypeName(),
-                MediaType = item.MediaType,
-                MatchedTerm = hintInfo.MatchedTerm,
-                RunTimeTicks = item.RunTimeTicks,
-                ProductionYear = item.ProductionYear,
-                ChannelId = item.ChannelId,
-                EndDate = item.EndDate
-            };
-
-            // legacy
-            result.ItemId = result.Id;
-
-            if (item.IsFolder)
-            {
-                result.IsFolder = true;
-            }
-
-            var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
-
-            if (primaryImageTag != null)
-            {
-                result.PrimaryImageTag = primaryImageTag;
-                result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
-            }
-
-            SetThumbImageInfo(result, item);
-            SetBackdropImageInfo(result, item);
-
-            switch (item)
-            {
-                case IHasSeries hasSeries:
-                    result.Series = hasSeries.SeriesName;
-                    break;
-                case LiveTvProgram program:
-                    result.StartDate = program.StartDate;
-                    break;
-                case Series series:
-                    if (series.Status.HasValue)
-                    {
-                        result.Status = series.Status.Value.ToString();
-                    }
-
-                    break;
-                case MusicAlbum album:
-                    result.Artists = album.Artists;
-                    result.AlbumArtist = album.AlbumArtist;
-                    break;
-                case Audio song:
-                    result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
-                    result.Artists = song.Artists;
-
-                    MusicAlbum musicAlbum = song.AlbumEntity;
-
-                    if (musicAlbum != null)
-                    {
-                        result.Album = musicAlbum.Name;
-                        result.AlbumId = musicAlbum.Id;
-                    }
-                    else
-                    {
-                        result.Album = song.Album;
-                    }
-
-                    break;
-            }
-
-            if (!item.ChannelId.Equals(Guid.Empty))
-            {
-                var channel = _libraryManager.GetItemById(item.ChannelId);
-                result.ChannelName = channel?.Name;
-            }
-
-            return result;
-        }
-
-        private void SetThumbImageInfo(SearchHint hint, BaseItem item)
-        {
-            var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
-
-            if (itemWithImage == null && item is Episode)
-            {
-                itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
-            }
-
-            if (itemWithImage == null)
-            {
-                itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
-            }
-
-            if (itemWithImage != null)
-            {
-                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
-
-                if (tag != null)
-                {
-                    hint.ThumbImageTag = tag;
-                    hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
-                }
-            }
-        }
-
-        private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
-        {
-            var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
-                ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
-
-            if (itemWithImage != null)
-            {
-                var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
-
-                if (tag != null)
-                {
-                    hint.BackdropImageTag = tag;
-                    hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
-                }
-            }
-        }
-
-        private T GetParentWithImage<T>(BaseItem item, ImageType type)
-            where T : BaseItem
-        {
-            return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs
deleted file mode 100644
index 50adc56980..0000000000
--- a/MediaBrowser.Api/Sessions/SessionService.cs
+++ /dev/null
@@ -1,499 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Sessions
-{
-    /// <summary>
-    /// Class GetSessions.
-    /// </summary>
-    [Route("/Sessions", "GET", Summary = "Gets a list of sessions")]
-    [Authenticated]
-    public class GetSessions : IReturn<SessionInfo[]>
-    {
-        [ApiMember(Name = "ControllableByUserId", Description = "Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid ControllableByUserId { get; set; }
-
-        [ApiMember(Name = "DeviceId", Description = "Filter by device Id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string DeviceId { get; set; }
-
-        public int? ActiveWithinSeconds { get; set; }
-    }
-
-    /// <summary>
-    /// Class DisplayContent.
-    /// </summary>
-    [Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
-    [Authenticated]
-    public class DisplayContent : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Artist, Genre, Studio, Person, or any kind of BaseItem.
-        /// </summary>
-        /// <value>The type of the item.</value>
-        [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemType { get; set; }
-
-        /// <summary>
-        /// Artist name, genre name, item Id, etc.
-        /// </summary>
-        /// <value>The item identifier.</value>
-        [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the item.
-        /// </summary>
-        /// <value>The name of the item.</value>
-        [ApiMember(Name = "ItemName", Description = "The name of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemName { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")]
-    [Authenticated]
-    public class Play : PlayRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")]
-    [Authenticated]
-    public class SendPlaystateCommand : PlaystateRequest, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendSystemCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the command.
-        /// </summary>
-        /// <value>The play command.</value>
-        [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Command { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendGeneralCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the command.
-        /// </summary>
-        /// <value>The play command.</value>
-        [ApiMember(Name = "Command", Description = "The command to send.", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Command { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")]
-    [Authenticated]
-    public class SendFullGeneralCommand : GeneralCommand, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")]
-    [Authenticated]
-    public class SendMessageCommand : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Text", Description = "The message text.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Text { get; set; }
-
-        [ApiMember(Name = "Header", Description = "The message header.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Header { get; set; }
-
-        [ApiMember(Name = "TimeoutMs", Description = "The message timeout. If omitted the user will have to confirm viewing the message.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public long? TimeoutMs { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")]
-    [Authenticated]
-    public class AddUserToSession : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "UserId Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")]
-    [Authenticated]
-    public class RemoveUserFromSession : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
-    [Authenticated]
-    public class PostCapabilities : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlayableMediaTypes { get; set; }
-
-        [ApiMember(Name = "SupportedCommands", Description = "A list of supported remote control commands, comma delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string SupportedCommands { get; set; }
-
-        [ApiMember(Name = "SupportsMediaControl", Description = "Determines whether media can be played remotely.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsMediaControl { get; set; }
-
-        [ApiMember(Name = "SupportsSync", Description = "Determines whether sync is supported.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsSync { get; set; }
-
-        [ApiMember(Name = "SupportsPersistentIdentifier", Description = "Determines whether the device supports a unique identifier.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool SupportsPersistentIdentifier { get; set; }
-
-        public PostCapabilities()
-        {
-            SupportsPersistentIdentifier = true;
-        }
-    }
-
-    [Route("/Sessions/Capabilities/Full", "POST", Summary = "Updates capabilities for a device")]
-    [Authenticated]
-    public class PostFullCapabilities : ClientCapabilities, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/Viewing", "POST", Summary = "Reports that a session is viewing an item")]
-    [Authenticated]
-    public class ReportViewing : IReturnVoid
-    {
-        [ApiMember(Name = "SessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string SessionId { get; set; }
-
-        [ApiMember(Name = "ItemId", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ItemId { get; set; }
-    }
-
-    [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
-    [Authenticated]
-    public class ReportSessionEnded : IReturnVoid
-    {
-    }
-
-    [Route("/Auth/Providers", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetAuthProviders : IReturn<NameIdPair[]>
-    {
-    }
-
-    [Route("/Auth/PasswordResetProviders", "GET")]
-    [Authenticated(Roles = "Admin")]
-    public class GetPasswordResetProviders : IReturn<NameIdPair[]>
-    {
-    }
-
-    /// <summary>
-    /// Class SessionsService.
-    /// </summary>
-    public class SessionService : BaseApiService
-    {
-        /// <summary>
-        /// The session manager.
-        /// </summary>
-        private readonly ISessionManager _sessionManager;
-
-        private readonly IUserManager _userManager;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IDeviceManager _deviceManager;
-        private readonly ISessionContext _sessionContext;
-
-        public SessionService(
-            ILogger<SessionService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ISessionManager sessionManager,
-            IUserManager userManager,
-            IAuthorizationContext authContext,
-            IDeviceManager deviceManager,
-            ISessionContext sessionContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _sessionManager = sessionManager;
-            _userManager = userManager;
-            _authContext = authContext;
-            _deviceManager = deviceManager;
-            _sessionContext = sessionContext;
-        }
-
-        public object Get(GetAuthProviders request)
-        {
-            return _userManager.GetAuthenticationProviders();
-        }
-
-        public object Get(GetPasswordResetProviders request)
-        {
-            return _userManager.GetPasswordResetProviders();
-        }
-
-        public void Post(ReportSessionEnded request)
-        {
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            _sessionManager.Logout(auth.Token);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSessions request)
-        {
-            var result = _sessionManager.Sessions;
-
-            if (!string.IsNullOrEmpty(request.DeviceId))
-            {
-                result = result.Where(i => string.Equals(i.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase));
-            }
-
-            if (!request.ControllableByUserId.Equals(Guid.Empty))
-            {
-                result = result.Where(i => i.SupportsRemoteControl);
-
-                var user = _userManager.GetUserById(request.ControllableByUserId);
-
-                if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
-                {
-                    result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
-                }
-
-                if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
-                {
-                    result = result.Where(i => !i.UserId.Equals(Guid.Empty));
-                }
-
-                if (request.ActiveWithinSeconds.HasValue && request.ActiveWithinSeconds.Value > 0)
-                {
-                    var minActiveDate = DateTime.UtcNow.AddSeconds(0 - request.ActiveWithinSeconds.Value);
-                    result = result.Where(i => i.LastActivityDate >= minActiveDate);
-                }
-
-                result = result.Where(i =>
-                {
-                    var deviceId = i.DeviceId;
-
-                    if (!string.IsNullOrWhiteSpace(deviceId))
-                    {
-                        if (!_deviceManager.CanAccessDevice(user, deviceId))
-                        {
-                            return false;
-                        }
-                    }
-
-                    return true;
-                });
-            }
-
-            return ToOptimizedResult(result.ToArray());
-        }
-
-        public Task Post(SendPlaystateCommand request)
-        {
-            return _sessionManager.SendPlaystateCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(DisplayContent request)
-        {
-            var command = new BrowseRequest
-            {
-                ItemId = request.ItemId,
-                ItemName = request.ItemName,
-                ItemType = request.ItemType
-            };
-
-            return _sessionManager.SendBrowseCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(SendSystemCommand request)
-        {
-            var name = request.Command;
-            if (Enum.TryParse(name, true, out GeneralCommandType commandType))
-            {
-                name = commandType.ToString();
-            }
-
-            var currentSession = GetSession(_sessionContext);
-            var command = new GeneralCommand
-            {
-                Name = name,
-                ControllingUserId = currentSession.UserId
-            };
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(SendMessageCommand request)
-        {
-            var command = new MessageCommand
-            {
-                Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header,
-                TimeoutMs = request.TimeoutMs,
-                Text = request.Text
-            };
-
-            return _sessionManager.SendMessageCommand(GetSession(_sessionContext).Id, request.Id, command, CancellationToken.None);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(Play request)
-        {
-            return _sessionManager.SendPlayCommand(GetSession(_sessionContext).Id, request.Id, request, CancellationToken.None);
-        }
-
-        public Task Post(SendGeneralCommand request)
-        {
-            var currentSession = GetSession(_sessionContext);
-
-            var command = new GeneralCommand
-            {
-                Name = request.Command,
-                ControllingUserId = currentSession.UserId
-            };
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
-        }
-
-        public Task Post(SendFullGeneralCommand request)
-        {
-            var currentSession = GetSession(_sessionContext);
-
-            request.ControllingUserId = currentSession.UserId;
-
-            return _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, request, CancellationToken.None);
-        }
-
-        public void Post(AddUserToSession request)
-        {
-            _sessionManager.AddAdditionalUser(request.Id, new Guid(request.UserId));
-        }
-
-        public void Delete(RemoveUserFromSession request)
-        {
-            _sessionManager.RemoveAdditionalUser(request.Id, new Guid(request.UserId));
-        }
-
-        public void Post(PostCapabilities request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Id))
-            {
-                request.Id = GetSession(_sessionContext).Id;
-            }
-
-            _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities
-            {
-                PlayableMediaTypes = SplitValue(request.PlayableMediaTypes, ','),
-                SupportedCommands = SplitValue(request.SupportedCommands, ','),
-                SupportsMediaControl = request.SupportsMediaControl,
-                SupportsSync = request.SupportsSync,
-                SupportsPersistentIdentifier = request.SupportsPersistentIdentifier
-            });
-        }
-
-        public void Post(PostFullCapabilities request)
-        {
-            if (string.IsNullOrWhiteSpace(request.Id))
-            {
-                request.Id = GetSession(_sessionContext).Id;
-            }
-
-            _sessionManager.ReportCapabilities(request.Id, request);
-        }
-
-        public void Post(ReportViewing request)
-        {
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            _sessionManager.ReportNowViewingItem(request.SessionId, request.ItemId);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs
deleted file mode 100644
index a70da8e56c..0000000000
--- a/MediaBrowser.Api/Subtitles/SubtitleService.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Subtitles
-{
-    [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
-    [Authenticated(Roles = "Admin")]
-    public class DeleteSubtitle
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
-        public int Index { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
-    [Authenticated]
-    public class SearchRemoteSubtitles : IReturn<RemoteSubtitleInfo[]>
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Language { get; set; }
-
-        public bool? IsPerfectMatch { get; set; }
-    }
-
-    [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
-    [Authenticated]
-    public class DownloadRemoteSubtitles : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SubtitleId { get; set; }
-    }
-
-    [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
-    [Authenticated]
-    public class GetRemoteSubtitles : IReturnVoid
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
-    public class GetSubtitle
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-
-        [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Format { get; set; }
-
-        [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public long StartPositionTicks { get; set; }
-
-        [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public long? EndPositionTicks { get; set; }
-
-        [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool CopyTimestamps { get; set; }
-
-        public bool AddVttTimeMap { get; set; }
-    }
-
-    [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
-    [Authenticated]
-    public class GetSubtitlePlaylist
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-
-        [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int SegmentLength { get; set; }
-    }
-
-    public class SubtitleService : BaseApiService
-    {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ISubtitleManager _subtitleManager;
-        private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IAuthorizationContext _authContext;
-
-        public SubtitleService(
-            ILogger<SubtitleService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            ILibraryManager libraryManager,
-            ISubtitleManager subtitleManager,
-            ISubtitleEncoder subtitleEncoder,
-            IMediaSourceManager mediaSourceManager,
-            IProviderManager providerManager,
-            IFileSystem fileSystem,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _libraryManager = libraryManager;
-            _subtitleManager = subtitleManager;
-            _subtitleEncoder = subtitleEncoder;
-            _mediaSourceManager = mediaSourceManager;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
-            _authContext = authContext;
-        }
-
-        public async Task<object> Get(GetSubtitlePlaylist request)
-        {
-            var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
-
-            var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
-
-            var builder = new StringBuilder();
-
-            var runtime = mediaSource.RunTimeTicks ?? -1;
-
-            if (runtime <= 0)
-            {
-                throw new ArgumentException("HLS Subtitles are not supported for this media.");
-            }
-
-            var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
-            if (segmentLengthTicks <= 0)
-            {
-                throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
-            }
-
-            builder.AppendLine("#EXTM3U");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
-            builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
-
-            long positionTicks = 0;
-
-            var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
-
-            while (positionTicks < runtime)
-            {
-                var remaining = runtime - positionTicks;
-                var lengthTicks = Math.Min(remaining, segmentLengthTicks);
-
-                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
-
-                var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
-
-                var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
-                    positionTicks.ToString(CultureInfo.InvariantCulture),
-                    endPositionTicks.ToString(CultureInfo.InvariantCulture),
-                    accessToken);
-
-                builder.AppendLine(url);
-
-                positionTicks += segmentLengthTicks;
-            }
-
-            builder.AppendLine("#EXT-X-ENDLIST");
-
-            return ResultFactory.GetResult(Request, builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-        }
-
-        public async Task<object> Get(GetSubtitle request)
-        {
-            if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
-            {
-                request.Format = "json";
-            }
-
-            if (string.IsNullOrEmpty(request.Format))
-            {
-                var item = (Video)_libraryManager.GetItemById(request.Id);
-
-                var idString = request.Id.ToString("N", CultureInfo.InvariantCulture);
-                var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null)
-                    .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString));
-
-                var subtitleStream = mediaSource.MediaStreams
-                    .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index);
-
-                return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false);
-            }
-
-            if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap)
-            {
-                using var stream = await GetSubtitles(request).ConfigureAwait(false);
-                using var reader = new StreamReader(stream);
-
-                var text = reader.ReadToEnd();
-
-                text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
-
-                return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format));
-            }
-
-            return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format));
-        }
-
-        private Task<Stream> GetSubtitles(GetSubtitle request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-
-            return _subtitleEncoder.GetSubtitles(item,
-                request.MediaSourceId,
-                request.Index,
-                request.Format,
-                request.StartPositionTicks,
-                request.EndPositionTicks ?? 0,
-                request.CopyTimestamps,
-                CancellationToken.None);
-        }
-
-        public async Task<object> Get(SearchRemoteSubtitles request)
-        {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
-
-            return await _subtitleManager.SearchSubtitles(video, request.Language, request.IsPerfectMatch, CancellationToken.None).ConfigureAwait(false);
-        }
-
-        public Task Delete(DeleteSubtitle request)
-        {
-            var item = _libraryManager.GetItemById(request.Id);
-            return _subtitleManager.DeleteSubtitles(item, request.Index);
-        }
-
-        public async Task<object> Get(GetRemoteSubtitles request)
-        {
-            var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false);
-
-            return ResultFactory.GetResult(Request, result.Stream, MimeTypes.GetMimeType("file." + result.Format));
-        }
-
-        public void Post(DownloadRemoteSubtitles request)
-        {
-            var video = (Video)_libraryManager.GetItemById(request.Id);
-
-            Task.Run(async () =>
-            {
-                try
-                {
-                    await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
-                        .ConfigureAwait(false);
-
-                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error downloading subtitles");
-                }
-            });
-        }
-    }
-}
diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs
deleted file mode 100644
index 17afa8e79c..0000000000
--- a/MediaBrowser.Api/SuggestionsService.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    [Route("/Users/{UserId}/Suggestions", "GET", Summary = "Gets items based on a query.")]
-    public class GetSuggestedItems : IReturn<QueryResult<BaseItemDto>>
-    {
-        public string MediaType { get; set; }
-
-        public string Type { get; set; }
-
-        public Guid UserId { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        public int? StartIndex { get; set; }
-
-        public int? Limit { get; set; }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaType ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (Type ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-    }
-
-    public class SuggestionsService : BaseApiService
-    {
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-
-        public SuggestionsService(
-            ILogger<SuggestionsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            IUserManager userManager,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetSuggestedItems request)
-        {
-            return GetResultItems(request);
-        }
-
-        private QueryResult<BaseItemDto> GetResultItems(GetSuggestedItems request)
-        {
-            var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var result = GetItems(request, user, dtoOptions);
-
-            var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = result.TotalRecordCount,
-                Items = dtoList
-            };
-        }
-
-        private QueryResult<BaseItem> GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions)
-        {
-            return _libraryManager.GetItemsResult(new InternalItemsQuery(user)
-            {
-                OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                IsVirtualItem = false,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                DtoOptions = dtoOptions,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                Recursive = true
-            });
-        }
-    }
-}
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
deleted file mode 100644
index 4afa6e114a..0000000000
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
-    [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
-    public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
-    {
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDate { get; set; }
-
-        public bool? HasUserId { get; set; }
-    }
-
-    [Authenticated(Roles = "Admin")]
-    public class ActivityLogService : BaseApiService
-    {
-        private readonly IActivityManager _activityManager;
-
-        public ActivityLogService(
-            ILogger<ActivityLogService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IActivityManager activityManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _activityManager = activityManager;
-        }
-
-        public object Get(GetActivityLogs request)
-        {
-            DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
-                (DateTime?)null :
-                DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
-            var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
-                entries => entries.Where(entry => entry.DateCreated >= minDate
-                                                  && (!request.HasUserId.HasValue || (request.HasUserId.Value
-                                                      ? entry.UserId != Guid.Empty
-                                                      : entry.UserId == Guid.Empty))));
-
-            var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
-
-            return ToOptimizedResult(result);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs
deleted file mode 100644
index e0e20d828e..0000000000
--- a/MediaBrowser.Api/System/SystemService.cs
+++ /dev/null
@@ -1,221 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.System
-{
-    /// <summary>
-    /// Class GetSystemInfo.
-    /// </summary>
-    [Route("/System/Info", "GET", Summary = "Gets information about the server")]
-    [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)]
-    public class GetSystemInfo : IReturn<SystemInfo>
-    {
-    }
-
-    [Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")]
-    public class GetPublicSystemInfo : IReturn<PublicSystemInfo>
-    {
-    }
-
-    [Route("/System/Ping", "POST")]
-    [Route("/System/Ping", "GET")]
-    public class PingSystem : IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class RestartApplication.
-    /// </summary>
-    [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
-    [Authenticated(Roles = "Admin", AllowLocal = true)]
-    public class RestartApplication
-    {
-    }
-
-    /// <summary>
-    /// This is currently not authenticated because the uninstaller needs to be able to shutdown the server.
-    /// </summary>
-    [Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
-    [Authenticated(Roles = "Admin", AllowLocal = true)]
-    public class ShutdownApplication
-    {
-    }
-
-    [Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")]
-    [Authenticated(Roles = "Admin")]
-    public class GetServerLogs : IReturn<LogFile[]>
-    {
-    }
-
-    [Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")]
-    [Authenticated]
-    public class GetEndpointInfo : IReturn<EndPointInfo>
-    {
-        public string Endpoint { get; set; }
-    }
-
-    [Route("/System/Logs/Log", "GET", Summary = "Gets a log file")]
-    [Authenticated(Roles = "Admin")]
-    public class GetLogFile
-    {
-        [ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Name { get; set; }
-    }
-
-    [Route("/System/WakeOnLanInfo", "GET", Summary = "Gets wake on lan information")]
-    [Authenticated]
-    public class GetWakeOnLanInfo : IReturn<WakeOnLanInfo[]>
-    {
-    }
-
-    /// <summary>
-    /// Class SystemInfoService.
-    /// </summary>
-    public class SystemService : BaseApiService
-    {
-        /// <summary>
-        /// The _app host.
-        /// </summary>
-        private readonly IServerApplicationHost _appHost;
-        private readonly IApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-
-        private readonly INetworkManager _network;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SystemService" /> class.
-        /// </summary>
-        /// <param name="appHost">The app host.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <exception cref="ArgumentNullException">jsonSerializer</exception>
-        public SystemService(
-            ILogger<SystemService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IServerApplicationHost appHost,
-            IFileSystem fileSystem,
-            INetworkManager network)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _appPaths = serverConfigurationManager.ApplicationPaths;
-            _appHost = appHost;
-            _fileSystem = fileSystem;
-            _network = network;
-        }
-
-        public object Post(PingSystem request)
-        {
-            return _appHost.Name;
-        }
-
-        public object Get(GetWakeOnLanInfo request)
-        {
-            var result = _appHost.GetWakeOnLanInfo();
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetServerLogs request)
-        {
-            IEnumerable<FileSystemMetadata> files;
-
-            try
-            {
-                files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
-            }
-            catch (IOException ex)
-            {
-                Logger.LogError(ex, "Error getting logs");
-                files = Enumerable.Empty<FileSystemMetadata>();
-            }
-
-            var result = files.Select(i => new LogFile
-            {
-                DateCreated = _fileSystem.GetCreationTimeUtc(i),
-                DateModified = _fileSystem.GetLastWriteTimeUtc(i),
-                Name = i.Name,
-                Size = i.Length
-            }).OrderByDescending(i => i.DateModified)
-                .ThenByDescending(i => i.DateCreated)
-                .ThenBy(i => i.Name)
-                .ToArray();
-
-            return ToOptimizedResult(result);
-        }
-
-        public Task<object> Get(GetLogFile request)
-        {
-            var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
-                .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
-
-            // For older files, assume fully static
-            var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
-
-            return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetSystemInfo request)
-        {
-            var result = await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Get(GetPublicSystemInfo request)
-        {
-            var result = await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(RestartApplication request)
-        {
-            _appHost.Restart();
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(ShutdownApplication request)
-        {
-            Task.Run(async () =>
-            {
-                await Task.Delay(100).ConfigureAwait(false);
-                await _appHost.Shutdown().ConfigureAwait(false);
-            });
-        }
-
-        public object Get(GetEndpointInfo request)
-        {
-            return ToOptimizedResult(new EndPointInfo
-            {
-                IsLocal = Request.IsLocal,
-                IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp)
-            });
-        }
-    }
-}
diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs
deleted file mode 100644
index 165abd613d..0000000000
--- a/MediaBrowser.Api/TvShowsService.cs
+++ /dev/null
@@ -1,497 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetNextUpEpisodes.
-    /// </summary>
-    [Route("/Shows/NextUp", "GET", Summary = "Gets a list of next up episodes")]
-    public class GetNextUpEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "SeriesId", Description = "Optional. Filter by series id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeriesId { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        public GetNextUpEpisodes()
-        {
-            EnableTotalRecordCount = true;
-        }
-    }
-
-    [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
-    public class GetUpcomingEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
-    [Route("/Shows/{Id}/Episodes", "GET", Summary = "Gets episodes for a tv season")]
-    public class GetEpisodes : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Season", Description = "Optional filter by season number.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public int? Season { get; set; }
-
-        [ApiMember(Name = "SeasonId", Description = "Optional. Filter by season id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SeasonId { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "StartItemId", Description = "Optional. Skip through the list until a given item is found.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string StartItemId { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public SortOrder? SortOrder { get; set; }
-    }
-
-    [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")]
-    public class GetSeasons : IReturn<QueryResult<BaseItemDto>>, IHasItemFields, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "Id", Description = "The series id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "IsSpecialSeason", Description = "Optional. Filter by special season.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsSpecialSeason { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-    }
-
-    /// <summary>
-    /// Class TvShowsService.
-    /// </summary>
-    [Authenticated]
-    public class TvShowsService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        private readonly IDtoService _dtoService;
-        private readonly ITVSeriesManager _tvSeriesManager;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="TvShowsService" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="userDataManager">The user data repository.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public TvShowsService(
-            ILogger<TvShowsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            ITVSeriesManager tvSeriesManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _tvSeriesManager = tvSeriesManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetUpcomingEpisodes request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
-            var options = GetDtoOptions(_authContext, request);
-
-            var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[] { typeof(Episode).Name },
-                OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
-                MinPremiereDate = minPremiereDate,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = options
-            });
-
-            var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = itemsResult.Count,
-                Items = returnItems
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetNextUpEpisodes request)
-        {
-            var options = GetDtoOptions(_authContext, request);
-
-            var result = _tvSeriesManager.GetNextUp(new NextUpQuery
-            {
-                Limit = request.Limit,
-                ParentId = request.ParentId,
-                SeriesId = request.SeriesId,
-                StartIndex = request.StartIndex,
-                UserId = request.UserId,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            }, options);
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
-
-            return ToOptimizedResult(new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = result.TotalRecordCount,
-                Items = returnItems
-            });
-        }
-
-        /// <summary>
-        /// Applies the paging.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <param name="startIndex">The start index.</param>
-        /// <param name="limit">The limit.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit)
-        {
-            // Start at
-            if (startIndex.HasValue)
-            {
-                items = items.Skip(startIndex.Value);
-            }
-
-            // Return limit
-            if (limit.HasValue)
-            {
-                items = items.Take(limit.Value);
-            }
-
-            return items;
-        }
-
-        public object Get(GetSeasons request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var series = GetSeries(request.Id);
-
-            if (series == null)
-            {
-                throw new ResourceNotFoundException("Series not found");
-            }
-
-            var seasons = series.GetItemList(new InternalItemsQuery(user)
-            {
-                IsMissing = request.IsMissing,
-                IsSpecialSeason = request.IsSpecialSeason,
-                AdjacentTo = request.AdjacentTo
-            });
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = returnItems.Count,
-                Items = returnItems
-            };
-        }
-
-        private Series GetSeries(string seriesId)
-        {
-            if (!string.IsNullOrWhiteSpace(seriesId))
-            {
-                return _libraryManager.GetItemById(seriesId) as Series;
-            }
-
-            return null;
-        }
-
-        public object Get(GetEpisodes request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            List<BaseItem> episodes;
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            if (!string.IsNullOrWhiteSpace(request.SeasonId))
-            {
-                if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season))
-                {
-                    throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
-                }
-
-                episodes = season.GetEpisodes(user, dtoOptions);
-            }
-            else if (request.Season.HasValue)
-            {
-                var series = GetSeries(request.Id);
-
-                if (series == null)
-                {
-                    throw new ResourceNotFoundException("Series not found");
-                }
-
-                var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value);
-
-                episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions);
-            }
-            else
-            {
-                var series = GetSeries(request.Id);
-
-                if (series == null)
-                {
-                    throw new ResourceNotFoundException("Series not found");
-                }
-
-                episodes = series.GetEpisodes(user, dtoOptions).ToList();
-            }
-
-            // Filter after the fact in case the ui doesn't want them
-            if (request.IsMissing.HasValue)
-            {
-                var val = request.IsMissing.Value;
-                episodes = episodes.Where(i => ((Episode)i).IsMissingEpisode == val).ToList();
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.StartItemId))
-            {
-                episodes = episodes.SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), request.StartItemId, StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-
-            // This must be the last filter
-            if (!string.IsNullOrEmpty(request.AdjacentTo))
-            {
-                episodes = UserViewBuilder.FilterForAdjacency(episodes, request.AdjacentTo).ToList();
-            }
-
-            if (string.Equals(request.SortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
-            {
-                episodes.Shuffle();
-            }
-
-            var returnItems = episodes;
-
-            if (request.StartIndex.HasValue || request.Limit.HasValue)
-            {
-                returnItems = ApplyPaging(episodes, request.StartIndex, request.Limit).ToList();
-            }
-
-            var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = episodes.Count,
-                Items = dtos
-            };
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
deleted file mode 100644
index 9875e02084..0000000000
--- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetArtists.
-    /// </summary>
-    [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")]
-    public class GetArtists : GetItemsByName
-    {
-    }
-
-    [Route("/Artists/AlbumArtists", "GET", Summary = "Gets all album artists from a given item, folder, or the entire library")]
-    public class GetAlbumArtists : GetItemsByName
-    {
-    }
-
-    [Route("/Artists/{Name}", "GET", Summary = "Gets an artist, by name")]
-    public class GetArtist : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class ArtistsService.
-    /// </summary>
-    [Authenticated]
-    public class ArtistsService : BaseItemsByNameService<MusicArtist>
-    {
-        public ArtistsService(
-            ILogger<ArtistsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetArtist request)
-        {
-            return GetItem(request);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetArtist request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetArtist(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetArtists request)
-        {
-            return GetResultSlim(request);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetAlbumArtists request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
deleted file mode 100644
index fd639caf11..0000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ /dev/null
@@ -1,388 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class BaseItemsByNameService.
-    /// </summary>
-    /// <typeparam name="TItemType">The type of the T item type.</typeparam>
-    public abstract class BaseItemsByNameService<TItemType> : BaseApiService
-        where TItemType : BaseItem, IItemByName
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BaseItemsByNameService{TItemType}" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="userDataRepository">The user data repository.</param>
-        /// <param name="dtoService">The dto service.</param>
-        protected BaseItemsByNameService(
-            ILogger<BaseItemsByNameService<TItemType>> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            UserManager = userManager;
-            LibraryManager = libraryManager;
-            UserDataRepository = userDataRepository;
-            DtoService = dtoService;
-            AuthorizationContext = authorizationContext;
-        }
-
-        /// <summary>
-        /// Gets the _user manager.
-        /// </summary>
-        protected IUserManager UserManager { get; }
-
-        /// <summary>
-        /// Gets the library manager.
-        /// </summary>
-        protected ILibraryManager LibraryManager { get; }
-
-        protected IUserDataManager UserDataRepository { get; }
-
-        protected IDtoService DtoService { get; }
-
-        protected IAuthorizationContext AuthorizationContext { get; }
-
-        protected BaseItem GetParentItem(GetItemsByName request)
-        {
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            return parentItem;
-        }
-
-        protected string GetParentItemViewType(GetItemsByName request)
-        {
-            var parent = GetParentItem(request);
-
-            if (parent is IHasCollectionType collectionFolder)
-            {
-                return collectionFolder.CollectionType;
-            }
-
-            return null;
-        }
-
-        protected QueryResult<BaseItemDto> GetResultSlim(GetItemsByName request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            User user = null;
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            var excludeItemTypes = request.GetExcludeItemTypes();
-            var includeItemTypes = request.GetIncludeItemTypes();
-            var mediaTypes = request.GetMediaTypes();
-
-            var query = new InternalItemsQuery(user)
-            {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
-                MediaTypes = mediaTypes,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                IsFavorite = request.IsFavorite,
-                NameLessThan = request.NameLessThan,
-                NameStartsWith = request.NameStartsWith,
-                NameStartsWithOrGreater = request.NameStartsWithOrGreater,
-                Tags = request.GetTags(),
-                OfficialRatings = request.GetOfficialRatings(),
-                Genres = request.GetGenres(),
-                GenreIds = GetGuids(request.GenreIds),
-                StudioIds = GetGuids(request.StudioIds),
-                Person = request.Person,
-                PersonIds = GetGuids(request.PersonIds),
-                PersonTypes = request.GetPersonTypes(),
-                Years = request.GetYears(),
-                MinCommunityRating = request.MinCommunityRating,
-                DtoOptions = dtoOptions,
-                SearchTerm = request.SearchTerm,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
-            };
-
-            if (!string.IsNullOrWhiteSpace(request.ParentId))
-            {
-                if (parentItem is Folder)
-                {
-                    query.AncestorIds = new[] { new Guid(request.ParentId) };
-                }
-                else
-                {
-                    query.ItemIds = new[] { new Guid(request.ParentId) };
-                }
-            }
-
-            // Studios
-            if (!string.IsNullOrEmpty(request.Studios))
-            {
-                query.StudioIds = request.Studios.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return LibraryManager.GetStudio(i);
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            var result = GetItems(request, query);
-
-            var dtos = result.Items.Select(i =>
-            {
-                var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user);
-
-                if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes))
-                {
-                    SetItemCounts(dto, i.Item2);
-                }
-
-                return dto;
-            });
-
-            return new QueryResult<BaseItemDto>
-            {
-                Items = dtos.ToArray(),
-                TotalRecordCount = result.TotalRecordCount
-            };
-        }
-
-        protected virtual QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return new QueryResult<(BaseItem, ItemCounts)>();
-        }
-
-        private void SetItemCounts(BaseItemDto dto, ItemCounts counts)
-        {
-            dto.ChildCount = counts.ItemCount;
-            dto.ProgramCount = counts.ProgramCount;
-            dto.SeriesCount = counts.SeriesCount;
-            dto.EpisodeCount = counts.EpisodeCount;
-            dto.MovieCount = counts.MovieCount;
-            dto.TrailerCount = counts.TrailerCount;
-            dto.AlbumCount = counts.AlbumCount;
-            dto.SongCount = counts.SongCount;
-            dto.ArtistCount = counts.ArtistCount;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{ItemsResult}.</returns>
-        protected QueryResult<BaseItemDto> GetResult(GetItemsByName request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            User user = null;
-            BaseItem parentItem;
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                user = UserManager.GetUserById(request.UserId);
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.GetUserRootFolder() : LibraryManager.GetItemById(request.ParentId);
-            }
-            else
-            {
-                parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
-            }
-
-            IList<BaseItem> items;
-
-            var excludeItemTypes = request.GetExcludeItemTypes();
-            var includeItemTypes = request.GetIncludeItemTypes();
-            var mediaTypes = request.GetMediaTypes();
-
-            var query = new InternalItemsQuery(user)
-            {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
-                MediaTypes = mediaTypes,
-                DtoOptions = dtoOptions
-            };
-
-            bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
-
-            if (parentItem.IsFolder)
-            {
-                var folder = (Folder)parentItem;
-
-                if (!request.UserId.Equals(Guid.Empty))
-                {
-                    items = request.Recursive ?
-                        folder.GetRecursiveChildren(user, query).ToList() :
-                        folder.GetChildren(user, true).Where(Filter).ToList();
-                }
-                else
-                {
-                    items = request.Recursive ?
-                        folder.GetRecursiveChildren(Filter) :
-                        folder.Children.Where(Filter).ToList();
-                }
-            }
-            else
-            {
-                items = new[] { parentItem }.Where(Filter).ToList();
-            }
-
-            var extractedItems = GetAllItems(request, items);
-
-            var filteredItems = LibraryManager.Sort(extractedItems, user, request.GetOrderBy());
-
-            var ibnItemsArray = filteredItems.ToList();
-
-            IEnumerable<BaseItem> ibnItems = ibnItemsArray;
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                TotalRecordCount = ibnItemsArray.Count
-            };
-
-            if (request.StartIndex.HasValue || request.Limit.HasValue)
-            {
-                if (request.StartIndex.HasValue)
-                {
-                    ibnItems = ibnItems.Skip(request.StartIndex.Value);
-                }
-
-                if (request.Limit.HasValue)
-                {
-                    ibnItems = ibnItems.Take(request.Limit.Value);
-                }
-            }
-
-            var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
-
-            var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
-
-            result.Items = dtos.Where(i => i != null).ToArray();
-
-            return result;
-        }
-
-        /// <summary>
-        /// Filters the items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="f">The f.</param>
-        /// <param name="excludeItemTypes">The exclude item types.</param>
-        /// <param name="includeItemTypes">The include item types.</param>
-        /// <param name="mediaTypes">The media types.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes)
-        {
-            // Exclude item types
-            if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            // Include item types
-            if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            // Include MediaTypes
-            if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Task{`0}}.</returns>
-        protected abstract IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items);
-    }
-
-    /// <summary>
-    /// Class GetItemsByName.
-    /// </summary>
-    public class GetItemsByName : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-        public GetItemsByName()
-        {
-            Recursive = true;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
deleted file mode 100644
index 344861a496..0000000000
--- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
+++ /dev/null
@@ -1,478 +0,0 @@
-using System;
-using System.Linq;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    public abstract class BaseItemsRequest : IHasDtoOptions
-    {
-        protected BaseItemsRequest()
-        {
-            EnableImages = true;
-            EnableTotalRecordCount = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the max offical rating.
-        /// </summary>
-        /// <value>The max offical rating.</value>
-        [ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MaxOfficialRating { get; set; }
-
-        [ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasThemeSong { get; set; }
-
-        [ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasThemeVideo { get; set; }
-
-        [ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasSubtitles { get; set; }
-
-        [ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasSpecialFeature { get; set; }
-
-        [ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasTrailer { get; set; }
-
-        [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string AdjacentTo { get; set; }
-
-        [ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MinIndexNumber { get; set; }
-
-        [ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ParentIndexNumber { get; set; }
-
-        [ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasParentalRating { get; set; }
-
-        [ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHD { get; set; }
-
-        public bool? Is4K { get; set; }
-
-        [ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string LocationTypes { get; set; }
-
-        [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeLocationTypes { get; set; }
-
-        [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsMissing { get; set; }
-
-        [ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsUnaired { get; set; }
-
-        [ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double? MinCommunityRating { get; set; }
-
-        [ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double? MinCriticRating { get; set; }
-
-        [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? AiredDuringSeason { get; set; }
-
-        [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinPremiereDate { get; set; }
-
-        [ApiMember(Name = "MinDateLastSaved", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDateLastSaved { get; set; }
-
-        [ApiMember(Name = "MinDateLastSavedForUser", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinDateLastSavedForUser { get; set; }
-
-        [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MaxPremiereDate { get; set; }
-
-        [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasOverview { get; set; }
-
-        [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasImdbId { get; set; }
-
-        [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasTmdbId { get; set; }
-
-        [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? HasTvdbId { get; set; }
-
-        [ApiMember(Name = "ExcludeItemIds", Description = "Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemIds { get; set; }
-
-        public bool EnableTotalRecordCount { get; set; }
-
-        /// <summary>
-        /// Skips over a given number of items within the results. Use for paging.
-        /// </summary>
-        /// <value>The start index.</value>
-        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? StartIndex { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return.
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-
-        /// <summary>
-        /// Whether or not to perform the query recursively.
-        /// </summary>
-        /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "Recursive", Description = "When searching within folders, this determines whether or not the search will be recursive. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool Recursive { get; set; }
-
-        public string SearchTerm { get; set; }
-
-        /// <summary>
-        /// Gets or sets the sort order.
-        /// </summary>
-        /// <value>The sort order.</value>
-        [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string SortOrder { get; set; }
-
-        /// <summary>
-        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
-        /// </summary>
-        /// <value>The parent id.</value>
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ParentId { get; set; }
-
-        /// <summary>
-        /// Fields to return within the items, in addition to basic information.
-        /// </summary>
-        /// <value>The fields.</value>
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        /// <summary>
-        /// Gets or sets the exclude item types.
-        /// </summary>
-        /// <value>The exclude item types.</value>
-        [ApiMember(Name = "ExcludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ExcludeItemTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the include item types.
-        /// </summary>
-        /// <value>The include item types.</value>
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        /// <summary>
-        /// Filters to apply to the results.
-        /// </summary>
-        /// <value>The filters.</value>
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Filters { get; set; }
-
-        /// <summary>
-        /// Gets or sets the Isfavorite option.
-        /// </summary>
-        /// <value>IsFavorite</value>
-        [ApiMember(Name = "IsFavorite", Description = "Optional filter by items that are marked as favorite, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFavorite { get; set; }
-
-        /// <summary>
-        /// Gets or sets the media types.
-        /// </summary>
-        /// <value>The media types.</value>
-        [ApiMember(Name = "MediaTypes", Description = "Optional filter by MediaType. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string MediaTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the image types.
-        /// </summary>
-        /// <value>The image types.</value>
-        [ApiMember(Name = "ImageTypes", Description = "Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ImageTypes { get; set; }
-
-        /// <summary>
-        /// What to sort the results by.
-        /// </summary>
-        /// <value>The sort by.</value>
-        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SortBy { get; set; }
-
-        [ApiMember(Name = "IsPlayed", Description = "Optional filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlayed { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific genres.
-        /// </summary>
-        /// <value>The genres.</value>
-        [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Genres { get; set; }
-
-        public string GenreIds { get; set; }
-
-        [ApiMember(Name = "OfficialRatings", Description = "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string OfficialRatings { get; set; }
-
-        [ApiMember(Name = "Tags", Description = "Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Tags { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific years.
-        /// </summary>
-        /// <value>The years.</value>
-        [ApiMember(Name = "Years", Description = "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Years { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing a specific person.
-        /// </summary>
-        /// <value>The person.</value>
-        [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Person { get; set; }
-
-        [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string PersonIds { get; set; }
-
-        /// <summary>
-        /// If the Person filter is used, this can also be used to restrict to a specific person type.
-        /// </summary>
-        /// <value>The type of the person.</value>
-        [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string PersonTypes { get; set; }
-
-        /// <summary>
-        /// Limit results to items containing specific studios.
-        /// </summary>
-        /// <value>The studios.</value>
-        [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Studios { get; set; }
-
-        [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string StudioIds { get; set; }
-
-        /// <summary>
-        /// Gets or sets the studios.
-        /// </summary>
-        /// <value>The studios.</value>
-        [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Artists { get; set; }
-
-        public string ExcludeArtistIds { get; set; }
-
-        [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string ArtistIds { get; set; }
-
-        public string AlbumArtistIds { get; set; }
-
-        public string ContributingArtistIds { get; set; }
-
-        [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Albums { get; set; }
-
-        public string AlbumIds { get; set; }
-
-        /// <summary>
-        /// Gets or sets the item ids.
-        /// </summary>
-        /// <value>The item ids.</value>
-        [ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Ids { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video types.
-        /// </summary>
-        /// <value>The video types.</value>
-        [ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string VideoTypes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the min offical rating.
-        /// </summary>
-        /// <value>The min offical rating.</value>
-        [ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string MinOfficialRating { get; set; }
-
-        [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? IsLocked { get; set; }
-
-        [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlaceHolder { get; set; }
-
-        [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public bool? HasOfficialRating { get; set; }
-
-        [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? CollapseBoxSetItems { get; set; }
-
-        public int? MinWidth { get; set; }
-
-        public int? MinHeight { get; set; }
-
-        public int? MaxWidth { get; set; }
-
-        public int? MaxHeight { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video formats.
-        /// </summary>
-        /// <value>The video formats.</value>
-        [ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? Is3D { get; set; }
-
-        /// <summary>
-        /// Gets or sets the series status.
-        /// </summary>
-        /// <value>The series status.</value>
-        [ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string SeriesStatus { get; set; }
-
-        [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameStartsWithOrGreater { get; set; }
-
-        [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameStartsWith { get; set; }
-
-        [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string NameLessThan { get; set; }
-
-        public string[] GetGenres()
-        {
-            return (Genres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetTags()
-        {
-            return (Tags ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetOfficialRatings()
-        {
-            return (OfficialRatings ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetMediaTypes()
-        {
-            return (MediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetIncludeItemTypes()
-        {
-            return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetExcludeItemTypes()
-        {
-            return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public int[] GetYears()
-        {
-            return (Years ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
-        }
-
-        public string[] GetStudios()
-        {
-            return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public string[] GetPersonTypes()
-        {
-            return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public VideoType[] GetVideoTypes()
-        {
-            return string.IsNullOrEmpty(VideoTypes)
-                ? Array.Empty<VideoType>()
-                : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                    .Select(v => Enum.Parse<VideoType>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the filters.
-        /// </summary>
-        /// <returns>IEnumerable{ItemFilter}.</returns>
-        public ItemFilter[] GetFilters()
-        {
-            var val = Filters;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ItemFilter>()
-                : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the image types.
-        /// </summary>
-        /// <returns>IEnumerable{ImageType}.</returns>
-        public ImageType[] GetImageTypes()
-        {
-            var val = ImageTypes;
-
-            return string.IsNullOrEmpty(val)
-                ? Array.Empty<ImageType>()
-                : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray();
-        }
-
-        /// <summary>
-        /// Gets the order by.
-        /// </summary>
-        /// <returns>IEnumerable{ItemSortBy}.</returns>
-        public ValueTuple<string, SortOrder>[] GetOrderBy()
-        {
-            return GetOrderBy(SortBy, SortOrder);
-        }
-
-        public static ValueTuple<string, SortOrder>[] GetOrderBy(string sortBy, string requestedSortOrder)
-        {
-            var val = sortBy;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return Array.Empty<ValueTuple<string, SortOrder>>();
-            }
-
-            var vals = val.Split(',');
-            if (string.IsNullOrWhiteSpace(requestedSortOrder))
-            {
-                requestedSortOrder = "Ascending";
-            }
-
-            var sortOrders = requestedSortOrder.Split(',');
-
-            var result = new ValueTuple<string, SortOrder>[vals.Length];
-
-            for (var i = 0; i < vals.Length; i++)
-            {
-                var sortOrderIndex = sortOrders.Length > i ? i : 0;
-
-                var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
-                var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
-                    ? MediaBrowser.Model.Entities.SortOrder.Descending
-                    : MediaBrowser.Model.Entities.SortOrder.Ascending;
-
-                result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
-            }
-
-            return result;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs
deleted file mode 100644
index 7bdfbac981..0000000000
--- a/MediaBrowser.Api/UserLibrary/GenresService.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetGenres.
-    /// </summary>
-    [Route("/Genres", "GET", Summary = "Gets all genres from a given item, folder, or the entire library")]
-    public class GetGenres : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetGenre.
-    /// </summary>
-    [Route("/Genres/{Name}", "GET", Summary = "Gets a genre, by name")]
-    public class GetGenre : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GenresService.
-    /// </summary>
-    [Authenticated]
-    public class GenresService : BaseItemsByNameService<Genre>
-    {
-        public GenresService(
-            ILogger<GenresService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGenre request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetGenre request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetGenre(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGenres request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            var viewType = GetParentItemViewType(request);
-
-            if (string.Equals(viewType, CollectionType.Music) || string.Equals(viewType, CollectionType.MusicVideos))
-            {
-                return LibraryManager.GetMusicGenres(query);
-            }
-
-            return LibraryManager.GetGenres(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
deleted file mode 100644
index 7efe0552c6..0000000000
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ /dev/null
@@ -1,514 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetItems.
-    /// </summary>
-    [Route("/Items", "GET", Summary = "Gets items based on a query.")]
-    [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")]
-    public class GetItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    [Route("/Users/{UserId}/Items/Resume", "GET", Summary = "Gets items based on a query.")]
-    public class GetResumeItems : BaseItemsRequest, IReturn<QueryResult<BaseItemDto>>
-    {
-    }
-
-    /// <summary>
-    /// Class ItemsService.
-    /// </summary>
-    [Authenticated]
-    public class ItemsService : BaseApiService
-    {
-        /// <summary>
-        /// The _user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILocalizationManager _localization;
-
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ItemsService" /> class.
-        /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="localization">The localization.</param>
-        /// <param name="dtoService">The dto service.</param>
-        public ItemsService(
-            ILogger<ItemsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            ILocalizationManager localization,
-            IDtoService dtoService,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _localization = localization;
-            _dtoService = dtoService;
-            _authContext = authContext;
-        }
-
-        public object Get(GetResumeItems request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId);
-
-            var options = GetDtoOptions(_authContext, request);
-
-            var ancestorIds = Array.Empty<Guid>();
-
-            var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
-            if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
-            {
-                ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
-                    .Where(i => i is Folder)
-                    .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
-                    .Select(i => i.Id)
-                    .ToArray();
-            }
-
-            var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
-            {
-                OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
-                IsResumable = true,
-                StartIndex = request.StartIndex,
-                Limit = request.Limit,
-                ParentId = parentIdGuid,
-                Recursive = true,
-                DtoOptions = options,
-                MediaTypes = request.GetMediaTypes(),
-                IsVirtualItem = false,
-                CollapseBoxSetItems = false,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                AncestorIds = ancestorIds,
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                ExcludeItemTypes = request.GetExcludeItemTypes(),
-                SearchTerm = request.SearchTerm
-            });
-
-            var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, options, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                StartIndex = request.StartIndex.GetValueOrDefault(),
-                TotalRecordCount = itemsResult.TotalRecordCount,
-                Items = returnItems
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItems request)
-        {
-            if (request == null)
-            {
-                throw new ArgumentNullException(nameof(request));
-            }
-
-            var result = GetItems(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        private QueryResult<BaseItemDto> GetItems(GetItems request)
-        {
-            var user = request.UserId == Guid.Empty ? null : _userManager.GetUserById(request.UserId);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = GetQueryResult(request, dtoOptions, user);
-
-            if (result == null)
-            {
-                throw new InvalidOperationException("GetItemsToSerialize returned null");
-            }
-
-            if (result.Items == null)
-            {
-                throw new InvalidOperationException("GetItemsToSerialize result.Items returned null");
-            }
-
-            var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                StartIndex = request.StartIndex.GetValueOrDefault(),
-                TotalRecordCount = result.TotalRecordCount,
-                Items = dtoList
-            };
-        }
-
-        /// <summary>
-        /// Gets the items to serialize.
-        /// </summary>
-        private QueryResult<BaseItem> GetQueryResult(GetItems request, DtoOptions dtoOptions, User user)
-        {
-            if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
-            {
-                request.ParentId = null;
-            }
-
-            BaseItem item = null;
-
-            if (!string.IsNullOrEmpty(request.ParentId))
-            {
-                item = _libraryManager.GetItemById(request.ParentId);
-            }
-
-            if (item == null)
-            {
-                item = _libraryManager.GetUserRootFolder();
-            }
-
-            if (!(item is Folder folder))
-            {
-                folder = _libraryManager.GetUserRootFolder();
-            }
-
-            if (folder is IHasCollectionType hasCollectionType
-                && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
-            {
-                request.Recursive = true;
-                request.IncludeItemTypes = "Playlist";
-            }
-
-            bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
-                    // Assume all folders inside an EnabledChannel are enabled
-                    || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
-
-            var collectionFolders = _libraryManager.GetCollectionFolders(item);
-            foreach (var collectionFolder in collectionFolders)
-            {
-                if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
-                    collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
-                    StringComparer.OrdinalIgnoreCase))
-                {
-                    isInEnabledFolder = true;
-                }
-            }
-
-            if (!(item is UserRootFolder)
-                && !isInEnabledFolder
-                && !user.HasPermission(PermissionKind.EnableAllFolders)
-                && !user.HasPermission(PermissionKind.EnableAllChannels))
-            {
-                Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
-                return new QueryResult<BaseItem>
-                {
-                    Items = Array.Empty<BaseItem>(),
-                    TotalRecordCount = 0,
-                    StartIndex = 0
-                };
-            }
-
-            if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
-            {
-                return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
-            }
-
-            var itemsArray = folder.GetChildren(user, true);
-            return new QueryResult<BaseItem>
-            {
-                Items = itemsArray,
-                TotalRecordCount = itemsArray.Count,
-                StartIndex = 0
-            };
-        }
-
-        private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, User user)
-        {
-            var query = new InternalItemsQuery(user)
-            {
-                IsPlayed = request.IsPlayed,
-                MediaTypes = request.GetMediaTypes(),
-                IncludeItemTypes = request.GetIncludeItemTypes(),
-                ExcludeItemTypes = request.GetExcludeItemTypes(),
-                Recursive = request.Recursive,
-                OrderBy = request.GetOrderBy(),
-
-                IsFavorite = request.IsFavorite,
-                Limit = request.Limit,
-                StartIndex = request.StartIndex,
-                IsMissing = request.IsMissing,
-                IsUnaired = request.IsUnaired,
-                CollapseBoxSetItems = request.CollapseBoxSetItems,
-                NameLessThan = request.NameLessThan,
-                NameStartsWith = request.NameStartsWith,
-                NameStartsWithOrGreater = request.NameStartsWithOrGreater,
-                HasImdbId = request.HasImdbId,
-                IsPlaceHolder = request.IsPlaceHolder,
-                IsLocked = request.IsLocked,
-                MinWidth = request.MinWidth,
-                MinHeight = request.MinHeight,
-                MaxWidth = request.MaxWidth,
-                MaxHeight = request.MaxHeight,
-                Is3D = request.Is3D,
-                HasTvdbId = request.HasTvdbId,
-                HasTmdbId = request.HasTmdbId,
-                HasOverview = request.HasOverview,
-                HasOfficialRating = request.HasOfficialRating,
-                HasParentalRating = request.HasParentalRating,
-                HasSpecialFeature = request.HasSpecialFeature,
-                HasSubtitles = request.HasSubtitles,
-                HasThemeSong = request.HasThemeSong,
-                HasThemeVideo = request.HasThemeVideo,
-                HasTrailer = request.HasTrailer,
-                IsHD = request.IsHD,
-                Is4K = request.Is4K,
-                Tags = request.GetTags(),
-                OfficialRatings = request.GetOfficialRatings(),
-                Genres = request.GetGenres(),
-                ArtistIds = GetGuids(request.ArtistIds),
-                AlbumArtistIds = GetGuids(request.AlbumArtistIds),
-                ContributingArtistIds = GetGuids(request.ContributingArtistIds),
-                GenreIds = GetGuids(request.GenreIds),
-                StudioIds = GetGuids(request.StudioIds),
-                Person = request.Person,
-                PersonIds = GetGuids(request.PersonIds),
-                PersonTypes = request.GetPersonTypes(),
-                Years = request.GetYears(),
-                ImageTypes = request.GetImageTypes(),
-                VideoTypes = request.GetVideoTypes(),
-                AdjacentTo = request.AdjacentTo,
-                ItemIds = GetGuids(request.Ids),
-                MinCommunityRating = request.MinCommunityRating,
-                MinCriticRating = request.MinCriticRating,
-                ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId),
-                ParentIndexNumber = request.ParentIndexNumber,
-                EnableTotalRecordCount = request.EnableTotalRecordCount,
-                ExcludeItemIds = GetGuids(request.ExcludeItemIds),
-                DtoOptions = dtoOptions,
-                SearchTerm = request.SearchTerm
-            };
-
-            if (!string.IsNullOrWhiteSpace(request.Ids) || !string.IsNullOrWhiteSpace(request.SearchTerm))
-            {
-                query.CollapseBoxSetItems = false;
-            }
-
-            foreach (var filter in request.GetFilters())
-            {
-                switch (filter)
-                {
-                    case ItemFilter.Dislikes:
-                        query.IsLiked = false;
-                        break;
-                    case ItemFilter.IsFavorite:
-                        query.IsFavorite = true;
-                        break;
-                    case ItemFilter.IsFavoriteOrLikes:
-                        query.IsFavoriteOrLiked = true;
-                        break;
-                    case ItemFilter.IsFolder:
-                        query.IsFolder = true;
-                        break;
-                    case ItemFilter.IsNotFolder:
-                        query.IsFolder = false;
-                        break;
-                    case ItemFilter.IsPlayed:
-                        query.IsPlayed = true;
-                        break;
-                    case ItemFilter.IsResumable:
-                        query.IsResumable = true;
-                        break;
-                    case ItemFilter.IsUnplayed:
-                        query.IsPlayed = false;
-                        break;
-                    case ItemFilter.Likes:
-                        query.IsLiked = true;
-                        break;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(request.MinDateLastSaved))
-            {
-                query.MinDateLastSaved = DateTime.Parse(request.MinDateLastSaved, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinDateLastSavedForUser))
-            {
-                query.MinDateLastSavedForUser = DateTime.Parse(request.MinDateLastSavedForUser, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MinPremiereDate))
-            {
-                query.MinPremiereDate = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(request.MaxPremiereDate))
-            {
-                query.MaxPremiereDate = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-            }
-
-            // Filter by Series Status
-            if (!string.IsNullOrEmpty(request.SeriesStatus))
-            {
-                query.SeriesStatuses = request.SeriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
-            }
-
-            // ExcludeLocationTypes
-            if (!string.IsNullOrEmpty(request.ExcludeLocationTypes))
-            {
-                var excludeLocationTypes = request.ExcludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray();
-                if (excludeLocationTypes.Contains(LocationType.Virtual))
-                {
-                    query.IsVirtualItem = false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(request.LocationTypes))
-            {
-                var requestedLocationTypes =
-                    request.LocationTypes.Split(',');
-
-                if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
-                {
-                    query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
-                }
-            }
-
-            // Min official rating
-            if (!string.IsNullOrWhiteSpace(request.MinOfficialRating))
-            {
-                query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating);
-            }
-
-            // Max official rating
-            if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating))
-            {
-                query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating);
-            }
-
-            // Artists
-            if (!string.IsNullOrEmpty(request.Artists))
-            {
-                query.ArtistIds = request.Artists.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return _libraryManager.GetArtist(i, new DtoOptions(false));
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrWhiteSpace(request.ExcludeArtistIds))
-            {
-                query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.AlbumIds))
-            {
-                query.AlbumIds = GetGuids(request.AlbumIds);
-            }
-
-            // Albums
-            if (!string.IsNullOrEmpty(request.Albums))
-            {
-                query.AlbumIds = request.Albums.Split('|').SelectMany(i =>
-                {
-                    return _libraryManager.GetItemIds(new InternalItemsQuery
-                    {
-                        IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
-                        Name = i,
-                        Limit = 1
-                    });
-                }).ToArray();
-            }
-
-            // Studios
-            if (!string.IsNullOrEmpty(request.Studios))
-            {
-                query.StudioIds = request.Studios.Split('|').Select(i =>
-                {
-                    try
-                    {
-                        return _libraryManager.GetStudio(i);
-                    }
-                    catch
-                    {
-                        return null;
-                    }
-                }).Where(i => i != null).Select(i => i.Id).ToArray();
-            }
-
-            // Apply default sorting if none requested
-            if (query.OrderBy.Count == 0)
-            {
-                // Albums by artist
-                if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
-                {
-                    query.OrderBy = new[]
-                    {
-                        new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending),
-                        new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
-                    };
-                }
-            }
-
-            return query;
-        }
-    }
-
-    /// <summary>
-    /// Class DateCreatedComparer.
-    /// </summary>
-    public class DateCreatedComparer : IComparer<BaseItem>
-    {
-        /// <summary>
-        /// Compares the specified x.
-        /// </summary>
-        /// <param name="x">The x.</param>
-        /// <param name="y">The y.</param>
-        /// <returns>System.Int32.</returns>
-        public int Compare(BaseItem x, BaseItem y)
-        {
-            return x.DateCreated.CompareTo(y.DateCreated);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs
deleted file mode 100644
index 7924339ed3..0000000000
--- a/MediaBrowser.Api/UserLibrary/PersonsService.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetPersons.
-    /// </summary>
-    [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")]
-    public class GetPersons : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetPerson.
-    /// </summary>
-    [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")]
-    public class GetPerson : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class PersonsService.
-    /// </summary>
-    [Authenticated]
-    public class PersonsService : BaseItemsByNameService<Person>
-    {
-        public PersonsService(
-            ILogger<PersonsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPerson request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetPerson request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetPerson(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPersons request)
-        {
-            return GetResultSlim(request);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            var items = LibraryManager.GetPeopleItems(new InternalPeopleQuery
-            {
-                PersonTypes = query.PersonTypes,
-                NameContains = query.NameContains ?? query.SearchTerm
-            });
-
-            if ((query.IsFavorite ?? false) && query.User != null)
-            {
-                items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList();
-            }
-
-            return new QueryResult<(BaseItem, ItemCounts)>
-            {
-                TotalRecordCount = items.Count,
-                Items = items.Take(query.Limit ?? int.MaxValue).Select(i => (i as BaseItem, new ItemCounts())).ToArray()
-            };
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs
deleted file mode 100644
index d809cc2e79..0000000000
--- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs
+++ /dev/null
@@ -1,456 +0,0 @@
-using System;
-using System.Globalization;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class MarkPlayedItem.
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")]
-    public class MarkPlayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string DatePlayed { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class MarkUnplayedItem.
-    /// </summary>
-    [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")]
-    public class MarkUnplayedItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Sessions/Playing", "POST", Summary = "Reports playback has started within a session")]
-    public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Progress", "POST", Summary = "Reports playback progress within a session")]
-    public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
-    {
-    }
-
-    [Route("/Sessions/Playing/Ping", "POST", Summary = "Pings a playback session")]
-    public class PingPlaybackSession : IReturnVoid
-    {
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    [Route("/Sessions/Playing/Stopped", "POST", Summary = "Reports playback has stopped within a session")]
-    public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class OnPlaybackStart.
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")]
-    public class OnPlaybackStart : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool CanSeek { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public PlayMethod PlayMethod { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    /// <summary>
-    /// Class OnPlaybackProgress.
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")]
-    public class OnPlaybackProgress : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string MediaSourceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public long? PositionTicks { get; set; }
-
-        [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsPaused { get; set; }
-
-        [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool IsMuted { get; set; }
-
-        [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? AudioStreamIndex { get; set; }
-
-        [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? SubtitleStreamIndex { get; set; }
-
-        [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
-        public int? VolumeLevel { get; set; }
-
-        [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public PlayMethod PlayMethod { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-
-        [ApiMember(Name = "RepeatMode", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public RepeatMode RepeatMode { get; set; }
-    }
-
-    /// <summary>
-    /// Class OnPlaybackStopped.
-    /// </summary>
-    [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")]
-    public class OnPlaybackStopped : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string NextMediaType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the position ticks.
-        /// </summary>
-        /// <value>The position ticks.</value>
-        [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
-        public long? PositionTicks { get; set; }
-
-        [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlaySessionId { get; set; }
-    }
-
-    [Authenticated]
-    public class PlaystateService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserDataManager _userDataRepository;
-        private readonly ILibraryManager _libraryManager;
-        private readonly ISessionManager _sessionManager;
-        private readonly ISessionContext _sessionContext;
-        private readonly IAuthorizationContext _authContext;
-
-        public PlaystateService(
-            ILogger<PlaystateService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserDataManager userDataRepository,
-            ILibraryManager libraryManager,
-            ISessionManager sessionManager,
-            ISessionContext sessionContext,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userDataRepository = userDataRepository;
-            _libraryManager = libraryManager;
-            _sessionManager = sessionManager;
-            _sessionContext = sessionContext;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(MarkPlayedItem request)
-        {
-            var result = MarkPlayed(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        private UserItemDataDto MarkPlayed(MarkPlayedItem request)
-        {
-            var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
-            DateTime? datePlayed = null;
-
-            if (!string.IsNullOrEmpty(request.DatePlayed))
-            {
-                datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
-            }
-
-            var session = GetSession(_sessionContext);
-
-            var dto = UpdatePlayedStatus(user, request.Id, true, datePlayed);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
-                UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed);
-            }
-
-            return dto;
-        }
-
-        private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
-        {
-            if (method == PlayMethod.Transcode)
-            {
-                var job = string.IsNullOrWhiteSpace(playSessionId) ? null : ApiEntryPoint.Instance.GetTranscodingJob(playSessionId);
-                if (job == null)
-                {
-                    return PlayMethod.DirectPlay;
-                }
-            }
-
-            return method;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackStart request)
-        {
-            Post(new ReportPlaybackStart
-            {
-                CanSeek = request.CanSeek,
-                ItemId = new Guid(request.Id),
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex,
-                PlayMethod = request.PlayMethod,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId
-            });
-        }
-
-        public void Post(ReportPlaybackStart request)
-        {
-            request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            var task = _sessionManager.OnPlaybackStart(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(OnPlaybackProgress request)
-        {
-            Post(new ReportPlaybackProgress
-            {
-                ItemId = new Guid(request.Id),
-                PositionTicks = request.PositionTicks,
-                IsMuted = request.IsMuted,
-                IsPaused = request.IsPaused,
-                MediaSourceId = request.MediaSourceId,
-                AudioStreamIndex = request.AudioStreamIndex,
-                SubtitleStreamIndex = request.SubtitleStreamIndex,
-                VolumeLevel = request.VolumeLevel,
-                PlayMethod = request.PlayMethod,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId,
-                RepeatMode = request.RepeatMode
-            });
-        }
-
-        public void Post(ReportPlaybackProgress request)
-        {
-            request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId);
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            var task = _sessionManager.OnPlaybackProgress(request);
-
-            Task.WaitAll(task);
-        }
-
-        public void Post(PingPlaybackSession request)
-        {
-            ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId, null);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(OnPlaybackStopped request)
-        {
-            return Post(new ReportPlaybackStopped
-            {
-                ItemId = new Guid(request.Id),
-                PositionTicks = request.PositionTicks,
-                MediaSourceId = request.MediaSourceId,
-                PlaySessionId = request.PlaySessionId,
-                LiveStreamId = request.LiveStreamId,
-                NextMediaType = request.NextMediaType
-            });
-        }
-
-        public async Task Post(ReportPlaybackStopped request)
-        {
-            Logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", request.PlaySessionId ?? string.Empty);
-
-            if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
-            {
-                await ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
-            }
-
-            request.SessionId = GetSession(_sessionContext).Id;
-
-            await _sessionManager.OnPlaybackStopped(request);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(MarkUnplayedItem request)
-        {
-            var task = MarkUnplayed(request);
-
-            return ToOptimizedResult(task);
-        }
-
-        private UserItemDataDto MarkUnplayed(MarkUnplayedItem request)
-        {
-            var user = _userManager.GetUserById(Guid.Parse(request.UserId));
-
-            var session = GetSession(_sessionContext);
-
-            var dto = UpdatePlayedStatus(user, request.Id, false, null);
-
-            foreach (var additionalUserInfo in session.AdditionalUsers)
-            {
-                var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
-
-                UpdatePlayedStatus(additionalUser, request.Id, false, null);
-            }
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Updates the played status.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
-        /// <param name="datePlayed">The date played.</param>
-        /// <returns>Task.</returns>
-        private UserItemDataDto UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
-        {
-            var item = _libraryManager.GetItemById(itemId);
-
-            if (wasPlayed)
-            {
-                item.MarkPlayed(user, datePlayed, true);
-            }
-            else
-            {
-                item.MarkUnplayed(user);
-            }
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs
deleted file mode 100644
index 66350955f5..0000000000
--- a/MediaBrowser.Api/UserLibrary/StudiosService.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetStudios.
-    /// </summary>
-    [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")]
-    public class GetStudios : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetStudio.
-    /// </summary>
-    [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")]
-    public class GetStudio : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class StudiosService.
-    /// </summary>
-    [Authenticated]
-    public class StudiosService : BaseItemsByNameService<Studio>
-    {
-        public StudiosService(
-            ILogger<StudiosService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetStudio request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetStudio request)
-        {
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            var item = GetStudio(request.Name, LibraryManager, dtoOptions);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetStudios request)
-        {
-            var result = GetResultSlim(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
-        {
-            return LibraryManager.GetStudios(query);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
deleted file mode 100644
index f9cbba4104..0000000000
--- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs
+++ /dev/null
@@ -1,575 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetItem.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")]
-    public class GetItem : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetItem.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")]
-    public class GetRootFolder : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetIntros.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")]
-    public class GetIntros : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the item id.
-        /// </summary>
-        /// <value>The item id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class MarkFavoriteItem.
-    /// </summary>
-    [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")]
-    public class MarkFavoriteItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UnmarkFavoriteItem.
-    /// </summary>
-    [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")]
-    public class UnmarkFavoriteItem : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class ClearUserItemRating.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")]
-    public class DeleteUserItemRating : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserItemRating.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")]
-    public class UpdateUserItemRating : IReturn<UserItemDataDto>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
-        /// </summary>
-        /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
-        [ApiMember(Name = "Likes", Description = "Whether the user likes the item or not. true/false", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")]
-        public bool Likes { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetLocalTrailers.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")]
-    public class GetLocalTrailers : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetSpecialFeatures.
-    /// </summary>
-    [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")]
-    public class GetSpecialFeatures : IReturn<BaseItemDto[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-    }
-
-    [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")]
-    public class GetLatestMedia : IReturn<BaseItemDto[]>, IHasDtoOptions
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int Limit { get; set; }
-
-        [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid ParentId { get; set; }
-
-        [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string Fields { get; set; }
-
-        [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
-        public string IncludeItemTypes { get; set; }
-
-        [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsFolder { get; set; }
-
-        [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsPlayed { get; set; }
-
-        [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool GroupItems { get; set; }
-
-        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableImages { get; set; }
-
-        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? ImageTypeLimit { get; set; }
-
-        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string EnableImageTypes { get; set; }
-
-        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? EnableUserData { get; set; }
-
-        public GetLatestMedia()
-        {
-            Limit = 20;
-            GroupItems = true;
-        }
-    }
-
-    /// <summary>
-    /// Class UserLibraryService.
-    /// </summary>
-    [Authenticated]
-    public class UserLibraryService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserDataManager _userDataRepository;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-        private readonly IUserViewManager _userViewManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IAuthorizationContext _authContext;
-
-        public UserLibraryService(
-            ILogger<UserLibraryService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IUserViewManager userViewManager,
-            IFileSystem fileSystem,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _userDataRepository = userDataRepository;
-            _dtoService = dtoService;
-            _userViewManager = userViewManager;
-            _fileSystem = fileSystem;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSpecialFeatures request)
-        {
-            var result = GetAsync(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetLatestMedia request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            if (!request.IsPlayed.HasValue)
-            {
-                if (user.HidePlayedInLatest)
-                {
-                    request.IsPlayed = false;
-                }
-            }
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var list = _userViewManager.GetLatestItems(new LatestItemsQuery
-            {
-                GroupItems = request.GroupItems,
-                IncludeItemTypes = ApiEntryPoint.Split(request.IncludeItemTypes, ',', true),
-                IsPlayed = request.IsPlayed,
-                Limit = request.Limit,
-                ParentId = request.ParentId,
-                UserId = request.UserId,
-            }, dtoOptions);
-
-            var dtos = list.Select(i =>
-            {
-                var item = i.Item2[0];
-                var childCount = 0;
-
-                if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
-                {
-                    item = i.Item1;
-                    childCount = i.Item2.Count;
-                }
-
-                var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-                dto.ChildCount = childCount;
-
-                return dto;
-            });
-
-            return ToOptimizedResult(dtos.ToArray());
-        }
-
-        private BaseItemDto[] GetAsync(GetSpecialFeatures request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                _libraryManager.GetUserRootFolder() :
-                _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = item
-                .GetExtras(BaseItem.DisplayExtraTypes)
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
-
-            return dtos.ToArray();
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetLocalTrailers request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
-                .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
-                .ToArray();
-
-            if (item is IHasTrailers hasTrailers)
-            {
-                var trailers = hasTrailers.GetTrailers();
-                var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
-                var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
-                dtosExtras.CopyTo(allTrailers, 0);
-                dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
-                return ToOptimizedResult(allTrailers);
-            }
-
-            return ToOptimizedResult(dtosExtras);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetItem request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
-        {
-            if (item is Person)
-            {
-                var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
-                var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
-
-                if (!hasMetdata)
-                {
-                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                    {
-                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                        ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                        ForceSave = performFullRefresh
-                    };
-
-                    await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetRootFolder request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = _libraryManager.GetUserRootFolder();
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Get(GetIntros request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var item = string.IsNullOrEmpty(request.Id) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(request.Id);
-
-            var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-
-            var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = dtos.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(MarkFavoriteItem request)
-        {
-            var dto = MarkFavorite(request.UserId, request.Id, true);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(UnmarkFavoriteItem request)
-        {
-            var dto = MarkFavorite(request.UserId, request.Id, false);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Marks the favorite.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
-        private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
-        {
-            var user = _userManager.GetUserById(userId);
-
-            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
-            // Get the user data for this item
-            var data = _userDataRepository.GetUserData(user, item);
-
-            // Set favorite status
-            data.IsFavorite = isFavorite;
-
-            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Delete(DeleteUserItemRating request)
-        {
-            var dto = UpdateUserItemRating(request.UserId, request.Id, null);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(UpdateUserItemRating request)
-        {
-            var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes);
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Updates the user item rating.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="likes">if set to <c>true</c> [likes].</param>
-        private UserItemDataDto UpdateUserItemRating(Guid userId, Guid itemId, bool? likes)
-        {
-            var user = _userManager.GetUserById(userId);
-
-            var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
-
-            // Get the user data for this item
-            var data = _userDataRepository.GetUserData(user, item);
-
-            data.Likes = likes;
-
-            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
-
-            return _userDataRepository.GetUserDataDto(item, user);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs
deleted file mode 100644
index 6f1620dddf..0000000000
--- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Library;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    [Route("/Users/{UserId}/Views", "GET")]
-    public class GetUserViews : IReturn<QueryResult<BaseItemDto>>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        [ApiMember(Name = "IncludeExternalContent", Description = "Whether or not to include external views such as channels or live tv", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "GET")]
-        public bool? IncludeExternalContent { get; set; }
-
-        public bool IncludeHidden { get; set; }
-
-        public string PresetViews { get; set; }
-    }
-
-    [Route("/Users/{UserId}/GroupingOptions", "GET")]
-    public class GetGroupingOptions : IReturn<SpecialViewOption[]>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    public class UserViewsService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-        private readonly IUserViewManager _userViewManager;
-        private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
-        private readonly ILibraryManager _libraryManager;
-
-        public UserViewsService(
-            ILogger<UserViewsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            IUserViewManager userViewManager,
-            IDtoService dtoService,
-            IAuthorizationContext authContext,
-            ILibraryManager libraryManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _userViewManager = userViewManager;
-            _dtoService = dtoService;
-            _authContext = authContext;
-            _libraryManager = libraryManager;
-        }
-
-        public object Get(GetUserViews request)
-        {
-            var query = new UserViewQuery
-            {
-                UserId = request.UserId
-            };
-
-            if (request.IncludeExternalContent.HasValue)
-            {
-                query.IncludeExternalContent = request.IncludeExternalContent.Value;
-            }
-
-            query.IncludeHidden = request.IncludeHidden;
-
-            if (!string.IsNullOrWhiteSpace(request.PresetViews))
-            {
-                query.PresetViews = request.PresetViews.Split(',');
-            }
-
-            var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
-            if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
-            }
-
-            var folders = _userViewManager.GetUserViews(query);
-
-            var dtoOptions = GetDtoOptions(_authContext, request);
-            var fields = dtoOptions.Fields.ToList();
-
-            fields.Add(ItemFields.PrimaryImageAspectRatio);
-            fields.Add(ItemFields.DisplayPreferencesId);
-            fields.Remove(ItemFields.BasicSyncInfo);
-            dtoOptions.Fields = fields.ToArray();
-
-            var user = _userManager.GetUserById(request.UserId);
-
-            var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
-                .ToArray();
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = dtos.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        public object Get(GetGroupingOptions request)
-        {
-            var user = _userManager.GetUserById(request.UserId);
-
-            var list = _libraryManager.GetUserRootFolder()
-                .GetChildren(user, true)
-                .OfType<Folder>()
-                .Where(UserView.IsEligibleForGrouping)
-                .Select(i => new SpecialViewOption
-                {
-                    Name = i.Name,
-                    Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
-                })
-            .OrderBy(i => i.Name)
-            .ToArray();
-
-            return ToOptimizedResult(list);
-        }
-    }
-
-    class SpecialViewOption
-    {
-        public string Name { get; set; }
-
-        public string Id { get; set; }
-    }
-}
diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs
deleted file mode 100644
index 0523f89fa7..0000000000
--- a/MediaBrowser.Api/UserLibrary/YearsService.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.UserLibrary
-{
-    /// <summary>
-    /// Class GetYears.
-    /// </summary>
-    [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")]
-    public class GetYears : GetItemsByName
-    {
-    }
-
-    /// <summary>
-    /// Class GetYear.
-    /// </summary>
-    [Route("/Years/{Year}", "GET", Summary = "Gets a year")]
-    public class GetYear : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the year.
-        /// </summary>
-        /// <value>The year.</value>
-        [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Year { get; set; }
-
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    /// <summary>
-    /// Class YearsService.
-    /// </summary>
-    [Authenticated]
-    public class YearsService : BaseItemsByNameService<Year>
-    {
-        public YearsService(
-            ILogger<YearsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IUserDataManager userDataRepository,
-            IDtoService dtoService,
-            IAuthorizationContext authorizationContext)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                userDataRepository,
-                dtoService,
-                authorizationContext)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetYear request)
-        {
-            var result = GetItem(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the item.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task{BaseItemDto}.</returns>
-        private BaseItemDto GetItem(GetYear request)
-        {
-            var item = LibraryManager.GetYear(request.Year);
-
-            var dtoOptions = GetDtoOptions(AuthorizationContext, request);
-
-            if (!request.UserId.Equals(Guid.Empty))
-            {
-                var user = UserManager.GetUserById(request.UserId);
-
-                return DtoService.GetBaseItemDto(item, dtoOptions, user);
-            }
-
-            return DtoService.GetBaseItemDto(item, dtoOptions);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetYears request)
-        {
-            var result = GetResult(request);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets all items.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="items">The items.</param>
-        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IList<BaseItem> items)
-        {
-            return items
-                .Select(i => i.ProductionYear ?? 0)
-                .Where(i => i > 0)
-                .Distinct()
-                .Select(year => LibraryManager.GetYear(year));
-        }
-    }
-}
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
deleted file mode 100644
index 6e9d788dc1..0000000000
--- a/MediaBrowser.Api/UserService.cs
+++ /dev/null
@@ -1,598 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class GetUsers.
-    /// </summary>
-    [Route("/Users", "GET", Summary = "Gets a list of users")]
-    [Authenticated]
-    public class GetUsers : IReturn<UserDto[]>
-    {
-        [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsHidden { get; set; }
-
-        [ApiMember(Name = "IsDisabled", Description = "Optional filter by IsDisabled=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsDisabled { get; set; }
-
-        [ApiMember(Name = "IsGuest", Description = "Optional filter by IsGuest=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? IsGuest { get; set; }
-    }
-
-    [Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")]
-    public class GetPublicUsers : IReturn<UserDto[]>
-    {
-    }
-
-    /// <summary>
-    /// Class GetUser.
-    /// </summary>
-    [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
-    [Authenticated(EscapeParentalControl = true)]
-    public class GetUser : IReturn<UserDto>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class DeleteUser.
-    /// </summary>
-    [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")]
-    [Authenticated(Roles = "Admin")]
-    public class DeleteUser : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class AuthenticateUser.
-    /// </summary>
-    [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")]
-    public class AuthenticateUser : IReturn<AuthenticationResult>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pw { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-    }
-
-    /// <summary>
-    /// Class AuthenticateUser.
-    /// </summary>
-    [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")]
-    public class AuthenticateUserByName : IReturn<AuthenticationResult>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Username { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        [ApiMember(Name = "Password", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-
-        [ApiMember(Name = "Pw", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pw { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserPassword.
-    /// </summary>
-    [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")]
-    [Authenticated]
-    public class UpdateUserPassword : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the password.
-        /// </summary>
-        /// <value>The password.</value>
-        public string CurrentPassword { get; set; }
-
-        public string CurrentPw { get; set; }
-
-        public string NewPw { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [reset password].
-        /// </summary>
-        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
-        public bool ResetPassword { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUserEasyPassword.
-    /// </summary>
-    [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")]
-    [Authenticated]
-    public class UpdateUserEasyPassword : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the new password.
-        /// </summary>
-        /// <value>The new password.</value>
-        public string NewPassword { get; set; }
-
-        public string NewPw { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [reset password].
-        /// </summary>
-        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
-        public bool ResetPassword { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUser.
-    /// </summary>
-    [Route("/Users/{Id}", "POST", Summary = "Updates a user")]
-    [Authenticated]
-    public class UpdateUser : UserDto, IReturnVoid
-    {
-    }
-
-    /// <summary>
-    /// Class UpdateUser.
-    /// </summary>
-    [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")]
-    [Authenticated(Roles = "admin")]
-    public class UpdateUserPolicy : UserPolicy, IReturnVoid
-    {
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class UpdateUser.
-    /// </summary>
-    [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")]
-    [Authenticated]
-    public class UpdateUserConfiguration : UserConfiguration, IReturnVoid
-    {
-        [ApiMember(Name = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class CreateUser.
-    /// </summary>
-    [Route("/Users/New", "POST", Summary = "Creates a user")]
-    [Authenticated(Roles = "Admin")]
-    public class CreateUserByName : IReturn<UserDto>
-    {
-        [ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Name { get; set; }
-
-        [ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Password { get; set; }
-    }
-
-    [Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")]
-    public class ForgotPassword : IReturn<ForgotPasswordResult>
-    {
-        [ApiMember(Name = "EnteredUsername", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string EnteredUsername { get; set; }
-    }
-
-    [Route("/Users/ForgotPassword/Pin", "POST", Summary = "Redeems a forgot password pin")]
-    public class ForgotPasswordPin : IReturn<PinRedeemResult>
-    {
-        [ApiMember(Name = "Pin", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
-        public string Pin { get; set; }
-    }
-
-    /// <summary>
-    /// Class UsersService.
-    /// </summary>
-    public class UserService : BaseApiService
-    {
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly IUserManager _userManager;
-        private readonly ISessionManager _sessionMananger;
-        private readonly INetworkManager _networkManager;
-        private readonly IDeviceManager _deviceManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public UserService(
-            ILogger<UserService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ISessionManager sessionMananger,
-            INetworkManager networkManager,
-            IDeviceManager deviceManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _sessionMananger = sessionMananger;
-            _networkManager = networkManager;
-            _deviceManager = deviceManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetPublicUsers request)
-        {
-            // If the startup wizard hasn't been completed then just return all users
-            if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted)
-            {
-                return Get(new GetUsers
-                {
-                    IsDisabled = false
-                });
-            }
-
-            return Get(new GetUsers
-            {
-                IsHidden = false,
-                IsDisabled = false
-            }, true, true);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetUsers request)
-        {
-            return Get(request, false, false);
-        }
-
-        private object Get(GetUsers request, bool filterByDevice, bool filterByNetwork)
-        {
-            var users = _userManager.Users;
-
-            if (request.IsDisabled.HasValue)
-            {
-                users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value);
-            }
-
-            if (request.IsHidden.HasValue)
-            {
-                users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value);
-            }
-
-            if (filterByDevice)
-            {
-                var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
-
-                if (!string.IsNullOrWhiteSpace(deviceId))
-                {
-                    users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
-                }
-            }
-
-            if (filterByNetwork)
-            {
-                if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
-                {
-                    users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
-                }
-            }
-
-            var result = users
-                .OrderBy(u => u.Username)
-                .Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
-                .ToArray();
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetUser request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            var result = _userManager.GetUserDto(user, Request.RemoteIp);
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Deletes the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Delete(DeleteUser request)
-        {
-            return DeleteAsync(request);
-        }
-
-        public Task DeleteAsync(DeleteUser request)
-        {
-            _userManager.DeleteUser(request.Id);
-            _sessionMananger.RevokeUserTokens(request.Id, null);
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Post(AuthenticateUser request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
-            {
-                throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
-            }
-
-            // Password should always be null
-            return Post(new AuthenticateUserByName
-            {
-                Username = user.Username,
-                Password = null,
-                Pw = request.Pw
-            });
-        }
-
-        public async Task<object> Post(AuthenticateUserByName request)
-        {
-            var auth = _authContext.GetAuthorizationInfo(Request);
-
-            try
-            {
-                var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
-                {
-                    App = auth.Client,
-                    AppVersion = auth.Version,
-                    DeviceId = auth.DeviceId,
-                    DeviceName = auth.Device,
-                    Password = request.Pw,
-                    PasswordSha1 = request.Password,
-                    RemoteEndPoint = Request.RemoteIp,
-                    Username = request.Username
-                }).ConfigureAwait(false);
-
-                return ToOptimizedResult(result);
-            }
-            catch (SecurityException e)
-            {
-                // rethrow adding IP address to message
-                throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public Task Post(UpdateUserPassword request)
-        {
-            return PostAsync(request);
-        }
-
-        public async Task PostAsync(UpdateUserPassword request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (request.ResetPassword)
-            {
-                await _userManager.ResetPassword(user).ConfigureAwait(false);
-            }
-            else
-            {
-                var success = await _userManager.AuthenticateUser(
-                    user.Username,
-                    request.CurrentPw,
-                    request.CurrentPassword,
-                    Request.RemoteIp,
-                    false).ConfigureAwait(false);
-
-                if (success == null)
-                {
-                    throw new ArgumentException("Invalid user or password entered.");
-                }
-
-                await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
-
-                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
-
-                _sessionMananger.RevokeUserTokens(user.Id, currentToken);
-            }
-        }
-
-        public void Post(UpdateUserEasyPassword request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, true);
-
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            if (request.ResetPassword)
-            {
-                _userManager.ResetEasyPassword(user);
-            }
-            else
-            {
-                _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public async Task Post(UpdateUser request)
-        {
-            var id = Guid.Parse(GetPathValue(1));
-
-            AssertCanUpdateUser(_authContext, _userManager, id, false);
-
-            var dtoUser = request;
-
-            var user = _userManager.GetUserById(id);
-
-            if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal))
-            {
-                await _userManager.UpdateUserAsync(user);
-                _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration);
-            }
-            else
-            {
-                await _userManager.RenameUser(user, dtoUser.Name).ConfigureAwait(false);
-
-                _userManager.UpdateConfiguration(dtoUser.Id, dtoUser.Configuration);
-            }
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public async Task<object> Post(CreateUserByName request)
-        {
-            var newUser = _userManager.CreateUser(request.Name);
-
-            // no need to authenticate password for new user
-            if (request.Password != null)
-            {
-                await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
-            }
-
-            var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
-
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(ForgotPassword request)
-        {
-            var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp);
-
-            var result = await _userManager.StartForgotPasswordProcess(request.EnteredUsername, isLocal).ConfigureAwait(false);
-
-            return result;
-        }
-
-        public async Task<object> Post(ForgotPasswordPin request)
-        {
-            var result = await _userManager.RedeemPasswordResetPin(request.Pin).ConfigureAwait(false);
-
-            return result;
-        }
-
-        public void Post(UpdateUserConfiguration request)
-        {
-            AssertCanUpdateUser(_authContext, _userManager, request.Id, false);
-
-            _userManager.UpdateConfiguration(request.Id, request);
-        }
-
-        public void Post(UpdateUserPolicy request)
-        {
-            var user = _userManager.GetUserById(request.Id);
-
-            // If removing admin access
-            if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
-            {
-                if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
-                {
-                    throw new ArgumentException("There must be at least one user in the system with administrative access.");
-                }
-            }
-
-            // If disabling
-            if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
-            {
-                throw new ArgumentException("Administrators cannot be disabled.");
-            }
-
-            // If disabling
-            if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
-            {
-                if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
-                {
-                    throw new ArgumentException("There must be at least one enabled user in the system.");
-                }
-
-                var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
-                _sessionMananger.RevokeUserTokens(user.Id, currentToken);
-            }
-
-            _userManager.UpdatePolicy(request.Id, request);
-        }
-    }
-}

From 230c54721db3c4f9110a8db778265a788644cabc Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Jul 2020 08:05:21 -0600
Subject: [PATCH 321/463] update post profile image

---
 Jellyfin.Api/Controllers/ImageController.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index c24c5e24c5..1322d77e96 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -100,6 +100,11 @@ namespace Jellyfin.Api.Controllers
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType.Split(';').FirstOrDefault();
             var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+            if (user.ProfileImage != null)
+            {
+                _userManager.ClearProfileImage(user);
+            }
+
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
 
             await _providerManager

From 6602b0dfb6e05dadd73dd2841c579d5e9f87be59 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 21 Jul 2020 13:17:08 -0600
Subject: [PATCH 322/463] Move ImageService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/ImageController.cs | 694 +++++++++++++++++---
 MediaBrowser.Api/Images/ImageRequest.cs     | 100 ---
 MediaBrowser.Api/Images/ImageService.cs     | 573 ----------------
 MediaBrowser.Api/MediaBrowser.Api.csproj    |   1 +
 4 files changed, 612 insertions(+), 756 deletions(-)
 delete mode 100644 MediaBrowser.Api/Images/ImageRequest.cs
 delete mode 100644 MediaBrowser.Api/Images/ImageService.cs

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 1322d77e96..f89601d17b 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -330,7 +330,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
         /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
         /// <param name="imageIndex">Image index.</param>
-        /// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</param>
         /// <response code="200">Image stream returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>
@@ -341,6 +340,8 @@ namespace Jellyfin.Api.Controllers
         [HttpHead("/Items/{itemId}/Images/{imageType}")]
         [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
         [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetItemImage(
             [FromRoute] Guid itemId,
             [FromRoute] ImageType imageType,
@@ -349,17 +350,16 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? width,
             [FromQuery] int? height,
             [FromQuery] int? quality,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] bool? cropWhitespace,
-            [FromQuery] string format,
-            [FromQuery] bool addPlayedIndicator,
+            [FromQuery] string? format,
+            [FromQuery] bool? addPlayedIndicator,
             [FromQuery] double? percentPlayed,
             [FromQuery] int? unplayedCount,
             [FromQuery] int? blur,
-            [FromQuery] string backgroundColor,
-            [FromQuery] string foregroundLayer,
-            [FromRoute] int? imageIndex = null,
-            [FromQuery] bool enableImageEnhancers = true)
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -385,7 +385,6 @@ namespace Jellyfin.Api.Controllers
                     blur,
                     backgroundColor,
                     foregroundLayer,
-                    enableImageEnhancers,
                     item,
                     Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
                 .ConfigureAwait(false);
@@ -396,7 +395,84 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="itemId">Item id.</param>
         /// <param name="imageType">Image type.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
         /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetItemImage2(
+            [FromRoute] Guid itemId,
+            [FromRoute] ImageType imageType,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromRoute] string tag,
+            [FromQuery] bool? cropWhitespace,
+            [FromRoute] string format,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    itemId,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get artist image by name.
+        /// </summary>
+        /// <param name="name">Artist name.</param>
+        /// <param name="imageType">Image type.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
         /// <param name="maxWidth">The maximum image width to return.</param>
@@ -411,19 +487,20 @@ namespace Jellyfin.Api.Controllers
         /// <param name="blur">Optional. Blur image.</param>
         /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
         /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
-        /// <param name="enableImageEnhancers">Enable or disable image enhancers such as cover art.</param>
+        /// <param name="imageIndex">Image index.</param>
         /// <response code="200">Image stream returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
-        public ActionResult<object> GetItemImage(
-            [FromRoute] Guid itemId,
+        [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetArtistImage(
+            [FromRoute] string name,
             [FromRoute] ImageType imageType,
-            [FromRoute] int? imageIndex,
             [FromRoute] string tag,
             [FromRoute] string format,
             [FromRoute] int? maxWidth,
@@ -434,39 +511,447 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? quality,
             [FromQuery] bool? cropWhitespace,
-            [FromQuery] bool addPlayedIndicator,
+            [FromQuery] bool? addPlayedIndicator,
             [FromQuery] int? blur,
-            [FromQuery] string backgroundColor,
-            [FromQuery] string foregroundLayer,
-            [FromQuery] bool enableImageEnhancers = true)
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
         {
-            var item = _libraryManager.GetItemById(itemId);
+            var item = _libraryManager.GetArtist(name);
             if (item == null)
             {
                 return NotFound();
             }
 
-            return GetImageInternal(
-                itemId,
-                imageType,
-                imageIndex,
-                tag,
-                format,
-                maxWidth,
-                maxHeight,
-                percentPlayed,
-                unplayedCount,
-                width,
-                height,
-                quality,
-                cropWhitespace,
-                addPlayedIndicator,
-                blur,
-                backgroundColor,
-                foregroundLayer,
-                enableImageEnhancers,
-                item,
-                Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase));
+            return await GetImageInternal(
+                    item.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get genre image by name.
+        /// </summary>
+        /// <param name="name">Genre name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetGenreImage(
+            [FromRoute] string name,
+            [FromRoute] ImageType imageType,
+            [FromRoute] string tag,
+            [FromRoute] string format,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetGenre(name);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    item.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get music genre image by name.
+        /// </summary>
+        /// <param name="name">Music genre name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetMusicGenreImage(
+            [FromRoute] string name,
+            [FromRoute] ImageType imageType,
+            [FromRoute] string tag,
+            [FromRoute] string format,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetMusicGenre(name);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    item.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get person image by name.
+        /// </summary>
+        /// <param name="name">Person name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetPersonImage(
+            [FromRoute] string name,
+            [FromRoute] ImageType imageType,
+            [FromRoute] string tag,
+            [FromRoute] string format,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetPerson(name);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    item.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get studio image by name.
+        /// </summary>
+        /// <param name="name">Studio name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetStudioImage(
+            [FromRoute] string name,
+            [FromRoute] ImageType imageType,
+            [FromRoute] string tag,
+            [FromRoute] string format,
+            [FromRoute] int? maxWidth,
+            [FromRoute] int? maxHeight,
+            [FromRoute] double? percentPlayed,
+            [FromRoute] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var item = _libraryManager.GetStudio(name);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            return await GetImageInternal(
+                    item.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    item,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Get user profile image.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> GetUserImage(
+            [FromRoute] Guid userId,
+            [FromRoute] ImageType imageType,
+            [FromQuery] string? tag,
+            [FromQuery] string? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromRoute] int? imageIndex = null)
+        {
+            var user = _userManager.GetUserById(userId);
+            if (user == null)
+            {
+                return NotFound();
+            }
+
+            var info = new ItemImageInfo
+            {
+                Path = user.ProfileImage.Path,
+                Type = ImageType.Profile,
+                DateModified = user.ProfileImage.LastModified
+            };
+
+            if (width.HasValue)
+            {
+                info.Width = width.Value;
+            }
+
+            if (height.HasValue)
+            {
+                info.Height = height.Value;
+            }
+
+            return await GetImageInternal(
+                    user.Id,
+                    imageType,
+                    imageIndex,
+                    tag,
+                    format,
+                    maxWidth,
+                    maxHeight,
+                    percentPlayed,
+                    unplayedCount,
+                    width,
+                    height,
+                    quality,
+                    cropWhitespace,
+                    addPlayedIndicator,
+                    blur,
+                    backgroundColor,
+                    foregroundLayer,
+                    null,
+                    Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
+                    info)
+                .ConfigureAwait(false);
         }
 
         private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
@@ -475,7 +960,7 @@ namespace Jellyfin.Api.Controllers
             var text = await reader.ReadToEndAsync().ConfigureAwait(false);
 
             var bytes = Convert.FromBase64String(text);
-            return new MemoryStream(bytes) {Position = 0};
+            return new MemoryStream(bytes) { Position = 0 };
         }
 
         private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
@@ -525,7 +1010,6 @@ namespace Jellyfin.Api.Controllers
             catch (Exception ex)
             {
                 _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
-
                 return null;
             }
         }
@@ -534,8 +1018,8 @@ namespace Jellyfin.Api.Controllers
             Guid itemId,
             ImageType imageType,
             int? imageIndex,
-            string tag,
-            string format,
+            string? tag,
+            string? format,
             int? maxWidth,
             int? maxHeight,
             double? percentPlayed,
@@ -544,13 +1028,13 @@ namespace Jellyfin.Api.Controllers
             int? height,
             int? quality,
             bool? cropWhitespace,
-            bool addPlayedIndicator,
+            bool? addPlayedIndicator,
             int? blur,
-            string backgroundColor,
-            string foregroundLayer,
-            bool enableImageEnhancers,
-            BaseItem item,
-            bool isHeadRequest)
+            string? backgroundColor,
+            string? foregroundLayer,
+            BaseItem? item,
+            bool isHeadRequest,
+            ItemImageInfo? imageInfo = null)
         {
             if (percentPlayed.HasValue)
             {
@@ -576,16 +1060,16 @@ namespace Jellyfin.Api.Controllers
                 unplayedCount = null;
             }
 
-            var imageInfo = item.GetImageInfo(imageType, imageIndex ?? 0);
             if (imageInfo == null)
             {
-                return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item.Name, imageType));
+                imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
+                if (imageInfo == null)
+                {
+                    return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
+                }
             }
 
-            if (!cropWhitespace.HasValue)
-            {
-                cropWhitespace = imageType == ImageType.Logo || imageType == ImageType.Art;
-            }
+            cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art;
 
             var outputFormats = GetOutputFormats(format);
 
@@ -596,7 +1080,11 @@ namespace Jellyfin.Api.Controllers
                 cacheDuration = TimeSpan.FromDays(365);
             }
 
-            var responseHeaders = new Dictionary<string, string> {{"transferMode.dlna.org", "Interactive"}, {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}};
+            var responseHeaders = new Dictionary<string, string>
+            {
+                { "transferMode.dlna.org", "Interactive" },
+                { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
+            };
 
             return await GetImageResult(
                 item,
@@ -621,12 +1109,12 @@ namespace Jellyfin.Api.Controllers
                 isHeadRequest).ConfigureAwait(false);
         }
 
-        private ImageFormat[] GetOutputFormats(string format)
+        private ImageFormat[] GetOutputFormats(string? format)
         {
             if (!string.IsNullOrWhiteSpace(format)
                 && Enum.TryParse(format, true, out ImageFormat parsedFormat))
             {
-                return new[] {parsedFormat};
+                return new[] { parsedFormat };
             }
 
             return GetClientSupportedFormats();
@@ -698,7 +1186,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         private async Task<ActionResult> GetImageResult(
-            BaseItem item,
+            BaseItem? item,
             Guid itemId,
             int? index,
             int? height,
@@ -706,12 +1194,12 @@ namespace Jellyfin.Api.Controllers
             int? maxWidth,
             int? quality,
             int? width,
-            bool addPlayedIndicator,
+            bool? addPlayedIndicator,
             double? percentPlayed,
             int? unplayedCount,
             int? blur,
-            string backgroundColor,
-            string foregroundLayer,
+            string? backgroundColor,
+            string? foregroundLayer,
             ItemImageInfo imageInfo,
             bool cropWhitespace,
             IReadOnlyCollection<ImageFormat> supportedFormats,
@@ -719,7 +1207,7 @@ namespace Jellyfin.Api.Controllers
             IDictionary<string, string> headers,
             bool isHeadRequest)
         {
-            if (!imageInfo.IsLocalFile)
+            if (!imageInfo.IsLocalFile && item != null)
             {
                 imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false);
             }
@@ -736,7 +1224,7 @@ namespace Jellyfin.Api.Controllers
                 MaxWidth = maxWidth,
                 Quality = quality ?? 100,
                 Width = width,
-                AddPlayedIndicator = addPlayedIndicator,
+                AddPlayedIndicator = addPlayedIndicator ?? false,
                 PercentPlayed = percentPlayed ?? 0,
                 UnplayedCount = unplayedCount,
                 Blur = blur,
@@ -745,23 +1233,63 @@ namespace Jellyfin.Api.Controllers
                 SupportedOutputFormats = supportedFormats
             };
 
-            var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
-
-            headers[HeaderNames.Vary] = HeaderNames.Accept;
-            /*
-             // TODO
-            return _resultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                CacheDuration = cacheDuration,
-                ResponseHeaders = headers,
-                ContentType = imageResult.Item2,
-                DateLastModified = imageResult.Item3,
-                IsHeadRequest = isHeadRequest,
-                Path = imageResult.Item1,
-                FileShare = FileShare.Read
-            });
-            */
-            return NoContent();
+            var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
+
+            var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
+            var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+
+            // if the parsing of the IfModifiedSince header was not successful, disable caching
+            if (!parsingSuccessful)
+            {
+                // disableCaching = true;
+            }
+
+            foreach (var (key, value) in headers)
+            {
+                Response.Headers.Add(key, value);
+            }
+
+            Response.ContentType = imageContentType;
+            Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+            Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
+
+            if (disableCaching)
+            {
+                Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
+                Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
+            }
+            else
+            {
+                if (cacheDuration.HasValue)
+                {
+                    Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
+                }
+                else
+                {
+                    Response.Headers.Add(HeaderNames.CacheControl, "public");
+                }
+
+                Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+
+                // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
+                if (!(dateImageModified > ifModifiedSinceHeader))
+                {
+                    if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+                    {
+                        Response.StatusCode = StatusCodes.Status304NotModified;
+                        return new ContentResult();
+                    }
+                }
+            }
+
+            // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+            if (isHeadRequest)
+            {
+                return NoContent();
+            }
+
+            var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
+            return File(stream, imageContentType);
         }
     }
 }
diff --git a/MediaBrowser.Api/Images/ImageRequest.cs b/MediaBrowser.Api/Images/ImageRequest.cs
deleted file mode 100644
index 0f3455548d..0000000000
--- a/MediaBrowser.Api/Images/ImageRequest.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Images
-{
-    /// <summary>
-    /// Class ImageRequest.
-    /// </summary>
-    public class ImageRequest : DeleteImageRequest
-    {
-        /// <summary>
-        /// The max width.
-        /// </summary>
-        [ApiMember(Name = "MaxWidth", Description = "The maximum image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxWidth { get; set; }
-
-        /// <summary>
-        /// The max height.
-        /// </summary>
-        [ApiMember(Name = "MaxHeight", Description = "The maximum image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? MaxHeight { get; set; }
-
-        /// <summary>
-        /// The width.
-        /// </summary>
-        [ApiMember(Name = "Width", Description = "The fixed image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Width { get; set; }
-
-        /// <summary>
-        /// The height.
-        /// </summary>
-        [ApiMember(Name = "Height", Description = "The fixed image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Height { get; set; }
-
-        /// <summary>
-        /// Gets or sets the quality.
-        /// </summary>
-        /// <value>The quality.</value>
-        [ApiMember(Name = "Quality", Description = "Optional quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Quality { get; set; }
-
-        /// <summary>
-        /// Gets or sets the tag.
-        /// </summary>
-        /// <value>The tag.</value>
-        [ApiMember(Name = "Tag", Description = "Optional. Supply the cache tag from the item object to receive strong caching headers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Tag { get; set; }
-
-        [ApiMember(Name = "CropWhitespace", Description = "Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool? CropWhitespace { get; set; }
-
-        [ApiMember(Name = "EnableImageEnhancers", Description = "Enable or disable image enhancers such as cover art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool EnableImageEnhancers { get; set; }
-
-        [ApiMember(Name = "Format", Description = "Determines the output foramt of the image - original,gif,jpg,png", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public string Format { get; set; }
-
-        [ApiMember(Name = "AddPlayedIndicator", Description = "Optional. Add a played indicator", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public bool AddPlayedIndicator { get; set; }
-
-        [ApiMember(Name = "PercentPlayed", Description = "Optional percent to render for the percent played overlay", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public double? PercentPlayed { get; set; }
-
-        [ApiMember(Name = "UnplayedCount", Description = "Optional unplayed count overlay to render", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? UnplayedCount { get; set; }
-
-        public int? Blur { get; set; }
-
-        [ApiMember(Name = "BackgroundColor", Description = "Optional. Apply a background color for transparent images.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string BackgroundColor { get; set; }
-
-        [ApiMember(Name = "ForegroundLayer", Description = "Optional. Apply a foreground layer on top of the image.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string ForegroundLayer { get; set; }
-
-        public ImageRequest()
-        {
-            EnableImageEnhancers = true;
-        }
-    }
-
-    /// <summary>
-    /// Class DeleteImageRequest.
-    /// </summary>
-    public class DeleteImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the type of the image.
-        /// </summary>
-        /// <value>The type of the image.</value>
-        [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET,POST,DELETE")]
-        public ImageType Type { get; set; }
-
-        /// <summary>
-        /// Gets or sets the index.
-        /// </summary>
-        /// <value>The index.</value>
-        [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET,POST,DELETE")]
-        public int? Index { get; set; }
-    }
-}
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
deleted file mode 100644
index 55859d9f17..0000000000
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ /dev/null
@@ -1,573 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using User = Jellyfin.Data.Entities.User;
-
-namespace MediaBrowser.Api.Images
-{
-    [Route("/Items/{Id}/Images/{Type}", "GET")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
-    [Route("/Items/{Id}/Images/{Type}", "HEAD")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}", "HEAD")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "GET")]
-    [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "HEAD")]
-    public class GetItemImage : ImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetPersonImage.
-    /// </summary>
-    [Route("/Artists/{Name}/Images/{Type}", "GET")]
-    [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")]
-    [Route("/Genres/{Name}/Images/{Type}", "GET")]
-    [Route("/Genres/{Name}/Images/{Type}/{Index}", "GET")]
-    [Route("/MusicGenres/{Name}/Images/{Type}", "GET")]
-    [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "GET")]
-    [Route("/Persons/{Name}/Images/{Type}", "GET")]
-    [Route("/Persons/{Name}/Images/{Type}/{Index}", "GET")]
-    [Route("/Studios/{Name}/Images/{Type}", "GET")]
-    [Route("/Studios/{Name}/Images/{Type}/{Index}", "GET")]
-    ////[Route("/Years/{Year}/Images/{Type}", "GET")]
-    ////[Route("/Years/{Year}/Images/{Type}/{Index}", "GET")]
-    [Route("/Artists/{Name}/Images/{Type}", "HEAD")]
-    [Route("/Artists/{Name}/Images/{Type}/{Index}", "HEAD")]
-    [Route("/Genres/{Name}/Images/{Type}", "HEAD")]
-    [Route("/Genres/{Name}/Images/{Type}/{Index}", "HEAD")]
-    [Route("/MusicGenres/{Name}/Images/{Type}", "HEAD")]
-    [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "HEAD")]
-    [Route("/Persons/{Name}/Images/{Type}", "HEAD")]
-    [Route("/Persons/{Name}/Images/{Type}/{Index}", "HEAD")]
-    [Route("/Studios/{Name}/Images/{Type}", "HEAD")]
-    [Route("/Studios/{Name}/Images/{Type}/{Index}", "HEAD")]
-    ////[Route("/Years/{Year}/Images/{Type}", "HEAD")]
-    ////[Route("/Years/{Year}/Images/{Type}/{Index}", "HEAD")]
-    public class GetItemByNameImage : ImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "Item name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetUserImage.
-    /// </summary>
-    [Route("/Users/{Id}/Images/{Type}", "GET")]
-    [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")]
-    [Route("/Users/{Id}/Images/{Type}", "HEAD")]
-    [Route("/Users/{Id}/Images/{Type}/{Index}", "HEAD")]
-    public class GetUserImage : ImageRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-    }
-
-    /// <summary>
-    /// Class ImageService.
-    /// </summary>
-    public class ImageService : BaseApiService
-    {
-        private readonly IUserManager _userManager;
-
-        private readonly ILibraryManager _libraryManager;
-
-        private readonly IProviderManager _providerManager;
-
-        private readonly IImageProcessor _imageProcessor;
-        private readonly IFileSystem _fileSystem;
-        private readonly IAuthorizationContext _authContext;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImageService" /> class.
-        /// </summary>
-        public ImageService(
-            ILogger<ImageService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IProviderManager providerManager,
-            IImageProcessor imageProcessor,
-            IFileSystem fileSystem,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _providerManager = providerManager;
-            _imageProcessor = imageProcessor;
-            _fileSystem = fileSystem;
-            _authContext = authContext;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItemImage request)
-        {
-            return GetImage(request, request.Id, null, false);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Head(GetItemImage request)
-        {
-            return GetImage(request, request.Id, null, true);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetUserImage request)
-        {
-            var item = _userManager.GetUserById(request.Id);
-
-            return GetImage(request, item, false);
-        }
-
-        public object Head(GetUserImage request)
-        {
-            var item = _userManager.GetUserById(request.Id);
-
-            return GetImage(request, item, true);
-        }
-
-        public object Get(GetItemByNameImage request)
-        {
-            var type = GetPathValue(0).ToString();
-
-            var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
-
-            return GetImage(request, item.Id, item, false);
-        }
-
-        public object Head(GetItemByNameImage request)
-        {
-            var type = GetPathValue(0).ToString();
-
-            var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false));
-
-            return GetImage(request, item.Id, item, true);
-        }
-
-        /// <summary>
-        /// Gets the image.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="item">The item.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <returns>System.Object.</returns>
-        /// <exception cref="ResourceNotFoundException"></exception>
-        public Task<object> GetImage(ImageRequest request, Guid itemId, BaseItem item, bool isHeadRequest)
-        {
-            if (request.PercentPlayed.HasValue)
-            {
-                if (request.PercentPlayed.Value <= 0)
-                {
-                    request.PercentPlayed = null;
-                }
-                else if (request.PercentPlayed.Value >= 100)
-                {
-                    request.PercentPlayed = null;
-                    request.AddPlayedIndicator = true;
-                }
-            }
-
-            if (request.PercentPlayed.HasValue)
-            {
-                request.UnplayedCount = null;
-            }
-
-            if (request.UnplayedCount.HasValue
-                && request.UnplayedCount.Value <= 0)
-            {
-                request.UnplayedCount = null;
-            }
-
-            if (item == null)
-            {
-                item = _libraryManager.GetItemById(itemId);
-
-                if (item == null)
-                {
-                    throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N", CultureInfo.InvariantCulture)));
-                }
-            }
-
-            var imageInfo = GetImageInfo(request, item);
-            if (imageInfo == null)
-            {
-                throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
-            }
-
-            bool cropWhitespace;
-            if (request.CropWhitespace.HasValue)
-            {
-                cropWhitespace = request.CropWhitespace.Value;
-            }
-            else
-            {
-                cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
-            }
-
-            var outputFormats = GetOutputFormats(request);
-
-            TimeSpan? cacheDuration = null;
-
-            if (!string.IsNullOrEmpty(request.Tag))
-            {
-                cacheDuration = TimeSpan.FromDays(365);
-            }
-
-            var responseHeaders = new Dictionary<string, string>
-            {
-                {"transferMode.dlna.org", "Interactive"},
-                {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
-            };
-
-            return GetImageResult(
-                item,
-                itemId,
-                request,
-                imageInfo,
-                cropWhitespace,
-                outputFormats,
-                cacheDuration,
-                responseHeaders,
-                isHeadRequest);
-        }
-
-        public Task<object> GetImage(ImageRequest request, User user, bool isHeadRequest)
-        {
-            var imageInfo = GetImageInfo(request, user);
-
-            TimeSpan? cacheDuration = null;
-
-            if (!string.IsNullOrEmpty(request.Tag))
-            {
-                cacheDuration = TimeSpan.FromDays(365);
-            }
-
-            var responseHeaders = new Dictionary<string, string>
-            {
-                {"transferMode.dlna.org", "Interactive"},
-                {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
-            };
-
-            var outputFormats = GetOutputFormats(request);
-
-            return GetImageResult(user.Id,
-                request,
-                imageInfo,
-                outputFormats,
-                cacheDuration,
-                responseHeaders,
-                isHeadRequest);
-        }
-
-        private async Task<object> GetImageResult(
-            Guid itemId,
-            ImageRequest request,
-            ItemImageInfo info,
-            IReadOnlyCollection<ImageFormat> supportedFormats,
-            TimeSpan? cacheDuration,
-            IDictionary<string, string> headers,
-            bool isHeadRequest)
-        {
-            info.Type = ImageType.Profile;
-            var options = new ImageProcessingOptions
-            {
-                CropWhiteSpace = true,
-                Height = request.Height,
-                ImageIndex = request.Index ?? 0,
-                Image = info,
-                Item = null, // Hack alert
-                ItemId = itemId,
-                MaxHeight = request.MaxHeight,
-                MaxWidth = request.MaxWidth,
-                Quality = request.Quality ?? 100,
-                Width = request.Width,
-                AddPlayedIndicator = request.AddPlayedIndicator,
-                PercentPlayed = 0,
-                UnplayedCount = request.UnplayedCount,
-                Blur = request.Blur,
-                BackgroundColor = request.BackgroundColor,
-                ForegroundLayer = request.ForegroundLayer,
-                SupportedOutputFormats = supportedFormats
-            };
-
-            var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
-
-            headers[HeaderNames.Vary] = HeaderNames.Accept;
-
-            return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                CacheDuration = cacheDuration,
-                ResponseHeaders = headers,
-                ContentType = imageResult.Item2,
-                DateLastModified = imageResult.Item3,
-                IsHeadRequest = isHeadRequest,
-                Path = imageResult.Item1,
-
-                FileShare = FileShare.Read
-
-            }).ConfigureAwait(false);
-        }
-
-        private async Task<object> GetImageResult(
-            BaseItem item,
-            Guid itemId,
-            ImageRequest request,
-            ItemImageInfo image,
-            bool cropwhitespace,
-            IReadOnlyCollection<ImageFormat> supportedFormats,
-            TimeSpan? cacheDuration,
-            IDictionary<string, string> headers,
-            bool isHeadRequest)
-        {
-            if (!image.IsLocalFile)
-            {
-                item ??= _libraryManager.GetItemById(itemId);
-                image = await _libraryManager.ConvertImageToLocal(item, image, request.Index ?? 0).ConfigureAwait(false);
-            }
-
-            var options = new ImageProcessingOptions
-            {
-                CropWhiteSpace = cropwhitespace,
-                Height = request.Height,
-                ImageIndex = request.Index ?? 0,
-                Image = image,
-                Item = item,
-                ItemId = itemId,
-                MaxHeight = request.MaxHeight,
-                MaxWidth = request.MaxWidth,
-                Quality = request.Quality ?? 100,
-                Width = request.Width,
-                AddPlayedIndicator = request.AddPlayedIndicator,
-                PercentPlayed = request.PercentPlayed ?? 0,
-                UnplayedCount = request.UnplayedCount,
-                Blur = request.Blur,
-                BackgroundColor = request.BackgroundColor,
-                ForegroundLayer = request.ForegroundLayer,
-                SupportedOutputFormats = supportedFormats
-            };
-
-            var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
-
-            headers[HeaderNames.Vary] = HeaderNames.Accept;
-
-            return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                CacheDuration = cacheDuration,
-                ResponseHeaders = headers,
-                ContentType = imageResult.Item2,
-                DateLastModified = imageResult.Item3,
-                IsHeadRequest = isHeadRequest,
-                Path = imageResult.Item1,
-
-                FileShare = FileShare.Read
-            }).ConfigureAwait(false);
-        }
-
-        private ImageFormat[] GetOutputFormats(ImageRequest request)
-        {
-            if (!string.IsNullOrWhiteSpace(request.Format)
-                && Enum.TryParse(request.Format, true, out ImageFormat format))
-            {
-                return new[] { format };
-            }
-
-            return GetClientSupportedFormats();
-        }
-
-        private ImageFormat[] GetClientSupportedFormats()
-        {
-            var supportedFormats = Request.AcceptTypes ?? Array.Empty<string>();
-            if (supportedFormats.Length > 0)
-            {
-                for (int i = 0; i < supportedFormats.Length; i++)
-                {
-                    int index = supportedFormats[i].IndexOf(';');
-                    if (index != -1)
-                    {
-                        supportedFormats[i] = supportedFormats[i].Substring(0, index);
-                    }
-                }
-            }
-
-            var acceptParam = Request.QueryString["accept"];
-
-            var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
-
-            if (!supportsWebP)
-            {
-                var userAgent = Request.UserAgent ?? string.Empty;
-                if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
-                    userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    supportsWebP = true;
-                }
-            }
-
-            var formats = new List<ImageFormat>(4);
-
-            if (supportsWebP)
-            {
-                formats.Add(ImageFormat.Webp);
-            }
-
-            formats.Add(ImageFormat.Jpg);
-            formats.Add(ImageFormat.Png);
-
-            if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
-            {
-                formats.Add(ImageFormat.Gif);
-            }
-
-            return formats.ToArray();
-        }
-
-        private bool SupportsFormat(IEnumerable<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
-        {
-            var mimeType = "image/" + format;
-
-            if (requestAcceptTypes.Contains(mimeType))
-            {
-                return true;
-            }
-
-            if (acceptAll && requestAcceptTypes.Contains("*/*"))
-            {
-                return true;
-            }
-
-            return string.Equals(Request.QueryString["accept"], format, StringComparison.OrdinalIgnoreCase);
-        }
-
-        /// <summary>
-        /// Gets the image path.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
-        {
-            var index = request.Index ?? 0;
-
-            return item.GetImageInfo(request.Type, index);
-        }
-
-        private static ItemImageInfo GetImageInfo(ImageRequest request, User user)
-        {
-            var info = new ItemImageInfo
-            {
-                Path = user.ProfileImage.Path,
-                Type = ImageType.Primary,
-                DateModified = user.ProfileImage.LastModified,
-            };
-
-            if (request.Width.HasValue)
-            {
-                info.Width = request.Width.Value;
-            }
-
-            if (request.Height.HasValue)
-            {
-                info.Height = request.Height.Value;
-            }
-
-            return info;
-        }
-
-        /// <summary>
-        /// Posts the image.
-        /// </summary>
-        /// <param name="entity">The entity.</param>
-        /// <param name="inputStream">The input stream.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="mimeType">Type of the MIME.</param>
-        /// <returns>Task.</returns>
-        public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
-        {
-            var memoryStream = await GetMemoryStream(inputStream);
-
-            // Handle image/png; charset=utf-8
-            mimeType = mimeType.Split(';').FirstOrDefault();
-
-            await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
-
-            entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
-        }
-
-        private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
-        {
-            using var reader = new StreamReader(inputStream);
-            var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
-            var bytes = Convert.FromBase64String(text);
-            return new MemoryStream(bytes)
-            {
-                Position = 0
-            };
-        }
-
-        private async Task PostImage(User user, Stream inputStream, string mimeType)
-        {
-            var memoryStream = await GetMemoryStream(inputStream);
-
-            // Handle image/png; charset=utf-8
-            mimeType = mimeType.Split(';').FirstOrDefault();
-            var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
-            if (user.ProfileImage != null)
-            {
-                _userManager.ClearProfileImage(user);
-            }
-            
-            user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
-
-            await _providerManager
-                .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
-                .ConfigureAwait(false);
-            await _userManager.UpdateUserAsync(user);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index d703bdb058..3f75a3b296 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -12,6 +12,7 @@
 
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs" />
+    <Compile Remove="Images\ImageService.cs" />
   </ItemGroup>
 
   <PropertyGroup>

From cbf5c682e93ce9e60a80b0130d04e4493f4cb684 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Tue, 21 Jul 2020 22:06:07 +0200
Subject: [PATCH 323/463] Change enum values

---
 Jellyfin.Api/Controllers/SyncPlayController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 99f828518f..c0544091c5 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
             {
-                Type = bufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
+                Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
                 When = when,
                 PositionTicks = positionTicks
             };
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
             {
-                Type = PlaybackRequestType.UpdatePing,
+                Type = PlaybackRequestType.Ping,
                 Ping = Convert.ToInt64(ping)
             };
             _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);

From 9996afbf25ee7025bd7d0d7bceb0dbd75253b6d7 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 10:20:51 +0200
Subject: [PATCH 324/463] Add response code documentation

---
 .../Controllers/SyncPlayController.cs         | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index c0544091c5..3f40c7309b 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Model.SyncPlay;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api.Controllers
@@ -42,8 +43,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Create a new SyncPlay group.
         /// </summary>
+        /// <response code="204">New group created.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("New")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateNewGroup()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -55,8 +58,10 @@ namespace Jellyfin.Api.Controllers
         /// Join an existing SyncPlay group.
         /// </summary>
         /// <param name="groupId">The sync play group id.</param>
+        /// <response code="204">Group join successful.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Join")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult JoinGroup([FromQuery, Required] Guid groupId)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -73,8 +78,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Leave the joined SyncPlay group.
         /// </summary>
+        /// <response code="204">Group leave successful.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Leave")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult LeaveGroup()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -86,8 +93,10 @@ namespace Jellyfin.Api.Controllers
         /// Gets all SyncPlay groups.
         /// </summary>
         /// <param name="filterItemId">Optional. Filter by item id.</param>
+        /// <response code="200">Groups returned.</response>
         /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
         [HttpGet("List")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<GroupInfoView>> GetSyncPlayGroups([FromQuery] Guid? filterItemId)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -97,8 +106,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Request play in SyncPlay group.
         /// </summary>
+        /// <response code="204">Play request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -113,8 +124,10 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Request pause in SyncPlay group.
         /// </summary>
+        /// <response code="204">Pause request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Pause()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -130,8 +143,10 @@ namespace Jellyfin.Api.Controllers
         /// Request seek in SyncPlay group.
         /// </summary>
         /// <param name="positionTicks">The playback position in ticks.</param>
+        /// <response code="204">Seek request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Seek([FromQuery] long positionTicks)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -150,8 +165,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="when">When the request has been made by the client.</param>
         /// <param name="positionTicks">The playback position in ticks.</param>
         /// <param name="bufferingDone">Whether the buffering is done.</param>
+        /// <response code="204">Buffering request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -169,8 +186,10 @@ namespace Jellyfin.Api.Controllers
         /// Update session ping.
         /// </summary>
         /// <param name="ping">The ping.</param>
+        /// <response code="204">Ping updated.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Ping([FromQuery] double ping)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);

From 07e56850beba99d3a5794a27280b96032880eb1e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 10:39:48 +0200
Subject: [PATCH 325/463] Remove caching and content length

---
 .../Helpers/FileStreamResponseHelpers.cs      | 91 +------------------
 1 file changed, 2 insertions(+), 89 deletions(-)

diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index e03cafe35d..6ba74d5901 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,7 +1,5 @@
 using System;
-using System.Globalization;
 using System.IO;
-using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
@@ -10,7 +8,6 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Primitives;
 using Microsoft.Net.Http.Headers;
 
 namespace Jellyfin.Api.Helpers
@@ -35,7 +32,6 @@ namespace Jellyfin.Api.Helpers
             CancellationTokenSource cancellationTokenSource)
         {
             HttpClient httpClient = new HttpClient();
-            var responseHeaders = controller.Response.Headers;
 
             if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
             {
@@ -45,13 +41,7 @@ namespace Jellyfin.Api.Helpers
             var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
             var contentType = response.Content.Headers.ContentType.ToString();
 
-            responseHeaders[HeaderNames.AcceptRanges] = "none";
-
-            // Seeing cases of -1 here
-            if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value >= 0)
-            {
-                responseHeaders[HeaderNames.ContentLength] = response.Content.Headers.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
-            }
+            controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
             if (isHeadRequest)
             {
@@ -74,7 +64,6 @@ namespace Jellyfin.Api.Helpers
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <returns>An <see cref="ActionResult"/> the file.</returns>
-        // TODO: caching doesn't work
         public static ActionResult GetStaticFileResult(
             string path,
             string contentType,
@@ -83,52 +72,7 @@ namespace Jellyfin.Api.Helpers
             bool isHeadRequest,
             ControllerBase controller)
         {
-            bool disableCaching = false;
-            if (controller.Request.Headers.TryGetValue(HeaderNames.CacheControl, out StringValues headerValue))
-            {
-                disableCaching = headerValue.FirstOrDefault().Contains("no-cache", StringComparison.InvariantCulture);
-            }
-
-            bool parsingSuccessful = DateTime.TryParseExact(controller.Request.Headers[HeaderNames.IfModifiedSince], "ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false), DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime ifModifiedSinceHeader);
-
-            // if the parsing of the IfModifiedSince header was not successfull, disable caching
-            if (!parsingSuccessful)
-            {
-                disableCaching = true;
-            }
-
             controller.Response.ContentType = contentType;
-            controller.Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateLastModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
-            controller.Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
-
-            if (disableCaching)
-            {
-                controller.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
-                controller.Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
-            }
-            else
-            {
-                if (cacheDuration.HasValue)
-                {
-                    controller.Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
-                }
-                else
-                {
-                    controller.Response.Headers.Add(HeaderNames.CacheControl, "public");
-                }
-
-                controller.Response.Headers.Add(HeaderNames.LastModified, dateLastModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
-
-                // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
-                if (!(dateLastModified > ifModifiedSinceHeader))
-                {
-                    if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
-                    {
-                        controller.Response.StatusCode = StatusCodes.Status304NotModified;
-                        return new ContentResult();
-                    }
-                }
-            }
 
             // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
             if (isHeadRequest)
@@ -164,27 +108,13 @@ namespace Jellyfin.Api.Helpers
             TranscodingJobType transcodingJobType,
             CancellationTokenSource cancellationTokenSource)
         {
-            IHeaderDictionary responseHeaders = controller.Response.Headers;
             // Use the command line args with a dummy playlist path
             var outputPath = state.OutputFilePath;
 
-            responseHeaders[HeaderNames.AcceptRanges] = "none";
+            controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
             var contentType = state.GetMimeType(outputPath);
 
-            // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
-            // TODO (from api-migration): Investigate if this is still neccessary as we migrated away from ServiceStack
-            var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
-
-            if (contentLength.HasValue)
-            {
-                responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            else
-            {
-                responseHeaders.Remove(HeaderNames.ContentLength);
-            }
-
             // Headers only
             if (isHeadRequest)
             {
@@ -215,22 +145,5 @@ namespace Jellyfin.Api.Helpers
                 transcodingLock.Release();
             }
         }
-
-        /// <summary>
-        /// Gets the length of the estimated content.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.Nullable{System.Int64}.</returns>
-        private static long? GetEstimatedContentLength(StreamState state)
-        {
-            var totalBitrate = state.TotalOutputBitrate ?? 0;
-
-            if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
-            {
-                return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
-            }
-
-            return null;
-        }
     }
 }

From eae665a9c410540bdbf3880e340fa1a7fb19be92 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 10:57:27 +0200
Subject: [PATCH 326/463] Add properties to StreamState to fix some errors

---
 Jellyfin.Api/Helpers/StreamingHelpers.cs      | 35 ++++++++--------
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  | 18 ++++-----
 .../Models/StreamingDtos/StreamState.cs       | 40 ++++++++++++++++---
 3 files changed, 62 insertions(+), 31 deletions(-)

diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index c88ec0b2f2..ee1f1efce2 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -92,7 +92,10 @@ namespace Jellyfin.Api.Helpers
             var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
             {
                 // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive
-                Request = request,
+                // Request = request,
+                DeviceId = deviceId,
+                PlaySessionId = playSessionId,
+                LiveStreamId = liveStreamId,
                 RequestedUrl = url,
                 UserAgent = request.Headers[HeaderNames.UserAgent],
                 EnableDlnaHeaders = enableDlnaHeaders
@@ -113,23 +116,23 @@ namespace Jellyfin.Api.Helpers
             }
             */
 
-            if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
+            if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.VideoCodec))
             {
-                state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+                state.SupportedVideoCodecs = state.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
             }
 
             if (!string.IsNullOrWhiteSpace(audioCodec))
             {
                 state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
+                state.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
                                            ?? state.SupportedAudioCodecs.FirstOrDefault();
             }
 
             if (!string.IsNullOrWhiteSpace(subtitleCodec))
             {
                 state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+                state.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
                                               ?? state.SupportedSubtitleCodecs.FirstOrDefault();
             }
 
@@ -203,7 +206,7 @@ namespace Jellyfin.Api.Helpers
 
             if (isVideoRequest)
             {
-                state.OutputVideoCodec = state.VideoRequest.VideoCodec;
+                state.OutputVideoCodec = state.VideoCodec;
                 state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
 
                 encodingHelper.TryStreamCopy(state);
@@ -288,7 +291,7 @@ namespace Jellyfin.Api.Helpers
 
             var audioCodec = state.ActualOutputAudioCodec;
 
-            if (state.VideoRequest == null)
+            if (!state.IsVideoRequest)
             {
                 responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
                     state.OutputContainer,
@@ -426,12 +429,10 @@ namespace Jellyfin.Api.Helpers
                 return ext;
             }
 
-            var isVideoRequest = state.VideoRequest != null;
-
             // Try to infer based on the desired video codec
-            if (isVideoRequest)
+            if (state.IsVideoRequest)
             {
-                var videoCodec = state.VideoRequest.VideoCodec;
+                var videoCodec = state.VideoCodec;
 
                 if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -456,9 +457,9 @@ namespace Jellyfin.Api.Helpers
             }
 
             // Try to infer based on the desired audio codec
-            if (!isVideoRequest)
+            if (!state.IsVideoRequest)
             {
-                var audioCodec = state.Request.AudioCodec;
+                var audioCodec = state.AudioCodec;
 
                 if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
                 {
@@ -531,7 +532,7 @@ namespace Jellyfin.Api.Helpers
             var audioCodec = state.ActualOutputAudioCodec;
             var videoCodec = state.ActualOutputVideoCodec;
 
-            var mediaProfile = state.VideoRequest == null
+            var mediaProfile = !state.IsVideoRequest
                 ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
                 : profile.GetVideoMediaProfile(
                     state.OutputContainer,
@@ -561,7 +562,7 @@ namespace Jellyfin.Api.Helpers
 
             if (!(@static.HasValue && @static.Value))
             {
-                var transcodingProfile = state.VideoRequest == null ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
+                var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
 
                 if (transcodingProfile != null)
                 {
@@ -569,7 +570,7 @@ namespace Jellyfin.Api.Helpers
                     // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
                     state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 
-                    if (state.VideoRequest != null)
+                    if (!state.IsVideoRequest)
                     {
                         state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
                         state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 9fbd5ec2db..4605c01831 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Helpers
                 job.BitRate = bitRate;
             }
 
-            var deviceId = state.Request.DeviceId;
+            var deviceId = state.DeviceId;
 
             if (!string.IsNullOrWhiteSpace(deviceId))
             {
@@ -525,12 +525,12 @@ namespace Jellyfin.Api.Helpers
 
             var transcodingJob = this.OnTranscodeBeginning(
                 outputPath,
-                state.Request.PlaySessionId,
+                state.PlaySessionId,
                 state.MediaSource.LiveStreamId,
                 Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
                 transcodingJobType,
                 process,
-                state.Request.DeviceId,
+                state.DeviceId,
                 state,
                 cancellationTokenSource);
 
@@ -647,12 +647,12 @@ namespace Jellyfin.Api.Helpers
         /// <returns>TranscodingJob.</returns>
         public TranscodingJobDto OnTranscodeBeginning(
             string path,
-            string playSessionId,
-            string liveStreamId,
+            string? playSessionId,
+            string? liveStreamId,
             string transcodingJobId,
             TranscodingJobType type,
             Process process,
-            string deviceId,
+            string? deviceId,
             StreamState state,
             CancellationTokenSource cancellationTokenSource)
         {
@@ -706,9 +706,9 @@ namespace Jellyfin.Api.Helpers
                 _transcodingLocks.Remove(path);
             }
 
-            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
+            if (!string.IsNullOrWhiteSpace(state.DeviceId))
             {
-                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
+                _sessionManager.ClearTranscodingInfo(state.DeviceId);
             }
         }
 
@@ -747,7 +747,7 @@ namespace Jellyfin.Api.Helpers
                 state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
-            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
+            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.LiveStreamId))
             {
                 var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
                     new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index b962e0ac79..db7cc6a75c 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -53,10 +53,10 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public TranscodingThrottler? TranscodingThrottler { get; set; }
 
-        /// <summary>
+        /*/// <summary>
         /// Gets the video request.
         /// </summary>
-        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
+        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;*/
 
         /// <summary>
         /// Gets or sets the direct stream provicer.
@@ -68,10 +68,10 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public string? WaitForPath { get; set; }
 
-        /// <summary>
+        /*/// <summary>
         /// Gets a value indicating whether the request outputs video.
         /// </summary>
-        public bool IsOutputVideo => Request is VideoStreamRequest;
+        public bool IsOutputVideo => Request is VideoStreamRequest;*/
 
         /// <summary>
         /// Gets the segment length.
@@ -161,6 +161,36 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public TranscodingJobDto? TranscodingJob { get; set; }
 
+        /// <summary>
+        /// Gets or sets the device id.
+        /// </summary>
+        public string? DeviceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the play session id.
+        /// </summary>
+        public string? PlaySessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the live stream id.
+        /// </summary>
+        public string? LiveStreamId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the video coded.
+        /// </summary>
+        public string? VideoCodec { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audio codec.
+        /// </summary>
+        public string? AudioCodec { get; set; }
+
+        /// <summary>
+        /// Gets or sets the subtitle codec.
+        /// </summary>
+        public string? SubtitleCodec { get; set; }
+
         /// <inheritdoc />
         public void Dispose()
         {
@@ -189,7 +219,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
             {
                 // REVIEW: Is this the right place for this?
                 if (MediaSource.RequiresClosing
-                    && string.IsNullOrWhiteSpace(Request.LiveStreamId)
+                    && string.IsNullOrWhiteSpace(LiveStreamId)
                     && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
                 {
                     _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();

From 5580df38e62ba75762da2f2b3ed4acd69b66e391 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 11:05:41 +0200
Subject: [PATCH 327/463] Cleanup after merge

---
 Emby.Server.Implementations/ApplicationHost.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 34a4cc575c..ad6cbe167f 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,7 +4,6 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -46,7 +45,6 @@ using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
-using Emby.Server.Implementations.SyncPlay;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Api;
 using MediaBrowser.Common;

From b006fd1b8f7a0c92e76a031ead630423ff393cc4 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 22 Jul 2020 08:03:45 -0600
Subject: [PATCH 328/463] apply review suggestions

---
 Jellyfin.Api/Controllers/ImageController.cs | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index f89601d17b..18220c5f34 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -79,9 +79,12 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imageType">(Unused) Image type.</param>
         /// <param name="index">(Unused) Image index.</param>
         /// <response code="204">Image updated.</response>
+        /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Users/{userId}/Images/{imageType}")]
         [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> PostUserImage(
@@ -122,12 +125,14 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imageType">(Unused) Image type.</param>
         /// <param name="index">(Unused) Image index.</param>
         /// <response code="204">Image deleted.</response>
+        /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("/Users/{userId}/Images/{itemType}")]
         [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
         public ActionResult DeleteUserImage(
             [FromRoute] Guid userId,
             [FromRoute] ImageType imageType,
@@ -188,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imageType">Image type.</param>
         /// <param name="imageIndex">(Unused) Image index.</param>
         /// <response code="204">Image saved.</response>
-        /// <response code="400">Item not found.</response>
+        /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpPost("/Items/{itemId}/Images/{imageType}")]
         [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]

From 15ac8095b4d7e4b87c420a8789aeaec600827b68 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 16:49:52 +0200
Subject: [PATCH 329/463] Apply suggestions from code review

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/SyncPlayController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 3f40c7309b..c240960e7b 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="204">Play request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost]
+        [HttpPost("Play")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play()
         {
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="204">Pause request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost]
+        [HttpPost("Pause")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Pause()
         {
@@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="positionTicks">The playback position in ticks.</param>
         /// <response code="204">Seek request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost]
+        [HttpPost("Seek")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Seek([FromQuery] long positionTicks)
         {
@@ -167,7 +167,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="bufferingDone">Whether the buffering is done.</param>
         /// <response code="204">Buffering request sent to all group members.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost]
+        [HttpPost("Buffering")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
         {

From 69e6dd2747df84dd732ecf89fea9118085f064ea Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 16:53:56 +0200
Subject: [PATCH 330/463] Update Jellyfin.Api/Controllers/SyncPlayController.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/SyncPlayController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index c240960e7b..55ed42227d 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="ping">The ping.</param>
         /// <response code="204">Ping updated.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost]
+        [HttpPost("Ping")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Ping([FromQuery] double ping)
         {

From 2ce97c022e9ceadea4b9b72053626eff7439ff91 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 16:57:06 +0200
Subject: [PATCH 331/463] Move AudioService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AudioController.cs   |  80 ++++++---
 .../Helpers/FileStreamResponseHelpers.cs      |  17 +-
 Jellyfin.Api/Helpers/StreamingHelpers.cs      | 158 +++++++++---------
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |  14 +-
 .../Models/StreamingDtos/StreamState.cs       |  67 ++------
 .../StreamingDtos/StreamingRequestDto.cs      |  45 +++++
 .../Models/StreamingDtos/VideoRequestDto.cs   |  19 +++
 7 files changed, 236 insertions(+), 164 deletions(-)
 create mode 100644 Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
 create mode 100644 Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 4d29d38807..81492ed4aa 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
@@ -141,10 +142,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/stream.{container}")]
-        [HttpGet("{itemId}/stream")]
-        [HttpHead("{itemId}/stream.{container}")]
+        [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}")]
+        [HttpHead("{itemId}/stream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetAudioStream(
             [FromRoute] Guid itemId,
@@ -201,21 +202,61 @@ namespace Jellyfin.Api.Controllers
 
             var cancellationTokenSource = new CancellationTokenSource();
 
+            StreamingRequestDto streamingRequest = new StreamingRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static.HasValue ? @static.Value : true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy.HasValue ? enableAutoStreamCopy.Value : true,
+                AllowAudioStreamCopy = allowAudioStreamCopy.HasValue ? allowAudioStreamCopy.Value : true,
+                AllowVideoStreamCopy = allowVideoStreamCopy.HasValue ? allowVideoStreamCopy.Value : true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames.HasValue ? breakOnNonKeyFrames.Value : false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc.HasValue ? requireAvc.Value : true,
+                DeInterlace = deInterlace.HasValue ? deInterlace.Value : true,
+                RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
             var state = await StreamingHelpers.GetStreamingState(
-                    itemId,
-                    startTimeTicks,
-                    audioCodec,
-                    subtitleCodec,
-                    videoCodec,
-                    @params,
-                    @static,
-                    container,
-                    liveStreamId,
-                    playSessionId,
-                    mediaSourceId,
-                    deviceId,
-                    deviceProfileId,
-                    audioBitRate,
+                    streamingRequest,
                     Request,
                     _authContext,
                     _mediaSourceManager,
@@ -230,7 +271,6 @@ namespace Jellyfin.Api.Controllers
                     _deviceManager,
                     _transcodingJobHelper,
                     _transcodingJobType,
-                    false,
                     cancellationTokenSource.Token)
                 .ConfigureAwait(false);
 
@@ -255,7 +295,7 @@ namespace Jellyfin.Api.Controllers
 
                 using (state)
                 {
-                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false);
+                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this).ConfigureAwait(false);
                 }
             }
 
@@ -297,8 +337,6 @@ namespace Jellyfin.Api.Controllers
                     return FileStreamResponseHelpers.GetStaticFileResult(
                         state.MediaPath,
                         contentType,
-                        _fileSystem.GetLastWriteTimeUtc(state.MediaPath),
-                        cacheDuration,
                         isHeadRequest,
                         this);
                 }
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 6ba74d5901..9f16b53236 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -23,13 +23,11 @@ namespace Jellyfin.Api.Helpers
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
-        /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
         /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
         public static async Task<ActionResult> GetStaticRemoteStreamResult(
             StreamState state,
             bool isHeadRequest,
-            ControllerBase controller,
-            CancellationTokenSource cancellationTokenSource)
+            ControllerBase controller)
         {
             HttpClient httpClient = new HttpClient();
 
@@ -59,16 +57,12 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="path">The path to the file.</param>
         /// <param name="contentType">The content type of the file.</param>
-        /// <param name="dateLastModified">The <see cref="DateTime"/> of the last modification of the file.</param>
-        /// <param name="cacheDuration">The cache duration of the file.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <returns>An <see cref="ActionResult"/> the file.</returns>
         public static ActionResult GetStaticFileResult(
             string path,
             string contentType,
-            DateTime dateLastModified,
-            TimeSpan? cacheDuration,
             bool isHeadRequest,
             ControllerBase controller)
         {
@@ -135,10 +129,11 @@ namespace Jellyfin.Api.Helpers
                     state.Dispose();
                 }
 
-                Stream stream = new MemoryStream();
-
-                await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false);
-                return controller.File(stream, contentType);
+                using (var memoryStream = new MemoryStream())
+                {
+                    await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                    return controller.File(memoryStream, contentType);
+                }
             }
             finally
             {
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index ee1f1efce2..71bf053f58 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -30,22 +30,29 @@ namespace Jellyfin.Api.Helpers
     /// </summary>
     public static class StreamingHelpers
     {
+        /// <summary>
+        /// Gets the current streaming state.
+        /// </summary>
+        /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
+        /// <param name="httpRequest">The <see cref="HttpRequest"/>.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
         public static async Task<StreamState> GetStreamingState(
-            Guid itemId,
-            long? startTimeTicks,
-            string? audioCodec,
-            string? subtitleCodec,
-            string? videoCodec,
-            string? @params,
-            bool? @static,
-            string? container,
-            string? liveStreamId,
-            string? playSessionId,
-            string? mediaSourceId,
-            string? deviceId,
-            string? deviceProfileId,
-            int? audioBitRate,
-            HttpRequest request,
+            StreamingRequestDto streamingRequest,
+            HttpRequest httpRequest,
             IAuthorizationContext authorizationContext,
             IMediaSourceManager mediaSourceManager,
             IUserManager userManager,
@@ -59,49 +66,43 @@ namespace Jellyfin.Api.Helpers
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
             TranscodingJobType transcodingJobType,
-            bool isVideoRequest,
             CancellationToken cancellationToken)
         {
             EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
             // Parse the DLNA time seek header
-            if (!startTimeTicks.HasValue)
+            if (!streamingRequest.StartTimeTicks.HasValue)
             {
-                var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
+                var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
 
-                startTimeTicks = ParseTimeSeekHeader(timeSeek);
+                streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
             }
 
-            if (!string.IsNullOrWhiteSpace(@params))
+            if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
             {
-                // What is this?
-                ParseParams(request);
+                ParseParams(streamingRequest);
             }
 
-            var streamOptions = ParseStreamOptions(request.Query);
+            streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
 
-            var url = request.Path.Value.Split('.').Last();
+            var url = httpRequest.Path.Value.Split('.').Last();
 
-            if (string.IsNullOrEmpty(audioCodec))
+            if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
             {
-                audioCodec = encodingHelper.InferAudioCodec(url);
+                streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
             }
 
-            var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) ||
-                                    string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
+            var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+                                    string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
 
             var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
             {
-                // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive
-                // Request = request,
-                DeviceId = deviceId,
-                PlaySessionId = playSessionId,
-                LiveStreamId = liveStreamId,
+                Request = streamingRequest,
                 RequestedUrl = url,
-                UserAgent = request.Headers[HeaderNames.UserAgent],
+                UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
                 EnableDlnaHeaders = enableDlnaHeaders
             };
 
-            var auth = authorizationContext.GetAuthorizationInfo(request);
+            var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
             if (!auth.UserId.Equals(Guid.Empty))
             {
                 state.User = userManager.GetUserById(auth.UserId);
@@ -116,27 +117,27 @@ namespace Jellyfin.Api.Helpers
             }
             */
 
-            if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.VideoCodec))
+            if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
             {
-                state.SupportedVideoCodecs = state.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+                state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
             }
 
-            if (!string.IsNullOrWhiteSpace(audioCodec))
+            if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
             {
-                state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
+                state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
                                            ?? state.SupportedAudioCodecs.FirstOrDefault();
             }
 
-            if (!string.IsNullOrWhiteSpace(subtitleCodec))
+            if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
             {
-                state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+                state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
                                               ?? state.SupportedSubtitleCodecs.FirstOrDefault();
             }
 
-            var item = libraryManager.GetItemById(itemId);
+            var item = libraryManager.GetItemById(streamingRequest.Id);
 
             state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
 
@@ -150,10 +151,10 @@ namespace Jellyfin.Api.Helpers
             */
 
             MediaSourceInfo? mediaSource = null;
-            if (string.IsNullOrWhiteSpace(liveStreamId))
+            if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
             {
-                var currentJob = !string.IsNullOrWhiteSpace(playSessionId)
-                    ? transcodingJobHelper.GetTranscodingJob(playSessionId)
+                var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
+                    ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
                     : null;
 
                 if (currentJob != null)
@@ -163,13 +164,13 @@ namespace Jellyfin.Api.Helpers
 
                 if (mediaSource == null)
                 {
-                    var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false);
+                    var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
 
-                    mediaSource = string.IsNullOrEmpty(mediaSourceId)
+                    mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
                         ? mediaSources[0]
-                        : mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture));
+                        : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture));
 
-                    if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId)
+                    if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id)
                     {
                         mediaSource = mediaSources[0];
                     }
@@ -177,7 +178,7 @@ namespace Jellyfin.Api.Helpers
             }
             else
             {
-                var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false);
+                var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
                 mediaSource = liveStreamInfo.Item1;
                 state.DirectStreamProvider = liveStreamInfo.Item2;
             }
@@ -186,28 +187,28 @@ namespace Jellyfin.Api.Helpers
 
             var containerInternal = Path.GetExtension(state.RequestedUrl);
 
-            if (string.IsNullOrEmpty(container))
+            if (string.IsNullOrEmpty(streamingRequest.Container))
             {
-                containerInternal = container;
+                containerInternal = streamingRequest.Container;
             }
 
             if (string.IsNullOrEmpty(containerInternal))
             {
-                containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
+                containerInternal = (streamingRequest.Static && streamingRequest.Static) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
             }
 
             state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
 
-            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream);
+            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
 
-            state.OutputAudioCodec = audioCodec;
+            state.OutputAudioCodec = streamingRequest.AudioCodec;
 
             state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
 
-            if (isVideoRequest)
+            if (state.VideoRequest != null)
             {
-                state.OutputVideoCodec = state.VideoCodec;
-                state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
+                state.OutputVideoCodec = state.Request.VideoCodec;
+                state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
 
                 encodingHelper.TryStreamCopy(state);
 
@@ -220,21 +221,21 @@ namespace Jellyfin.Api.Helpers
                         state.OutputVideoBitrate.Value,
                         state.VideoStream?.Codec,
                         state.OutputVideoCodec,
-                        videoRequest.MaxWidth,
-                        videoRequest.MaxHeight);
+                        state.VideoRequest.MaxWidth,
+                        state.VideoRequest.MaxHeight);
 
-                    videoRequest.MaxWidth = resolution.MaxWidth;
-                    videoRequest.MaxHeight = resolution.MaxHeight;
+                    state.VideoRequest.MaxWidth = resolution.MaxWidth;
+                    state.VideoRequest.MaxHeight = resolution.MaxHeight;
                 }
             }
 
-            ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static);
+            ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
 
             var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
                 ? GetOutputFileExtension(state)
                 : ('.' + state.OutputContainer);
 
-            state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId);
+            state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
 
             return state;
         }
@@ -319,7 +320,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="value">The time seek header string.</param>
         /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
-        public static long? ParseTimeSeekHeader(string value)
+        private static long? ParseTimeSeekHeader(string value)
         {
             if (string.IsNullOrWhiteSpace(value))
             {
@@ -375,7 +376,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="queryString">The query string.</param>
         /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
-        public static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
+        private static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
         {
             Dictionary<string, string> streamOptions = new Dictionary<string, string>();
             foreach (var param in queryString)
@@ -398,7 +399,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
         /// <param name="startTimeTicks">The start time in ticks.</param>
-        public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
+        private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
         {
             var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
             var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
@@ -420,7 +421,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="state">The state.</param>
         /// <returns>System.String.</returns>
-        public static string? GetOutputFileExtension(StreamState state)
+        private static string? GetOutputFileExtension(StreamState state)
         {
             var ext = Path.GetExtension(state.RequestedUrl);
 
@@ -432,7 +433,7 @@ namespace Jellyfin.Api.Helpers
             // Try to infer based on the desired video codec
             if (state.IsVideoRequest)
             {
-                var videoCodec = state.VideoCodec;
+                var videoCodec = state.Request.VideoCodec;
 
                 if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -459,7 +460,7 @@ namespace Jellyfin.Api.Helpers
             // Try to infer based on the desired audio codec
             if (!state.IsVideoRequest)
             {
-                var audioCodec = state.AudioCodec;
+                var audioCodec = state.Request.AudioCodec;
 
                 if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
                 {
@@ -570,7 +571,7 @@ namespace Jellyfin.Api.Helpers
                     // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
                     state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 
-                    if (!state.IsVideoRequest)
+                    if (state.VideoRequest != null)
                     {
                         state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
                         state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
@@ -583,11 +584,16 @@ namespace Jellyfin.Api.Helpers
         /// Parses the parameters.
         /// </summary>
         /// <param name="request">The request.</param>
-        private void ParseParams(StreamRequest request)
+        private static void ParseParams(StreamingRequestDto request)
         {
+            if (string.IsNullOrEmpty(request.Params))
+            {
+                return;
+            }
+
             var vals = request.Params.Split(';');
 
-            var videoRequest = request as VideoStreamRequest;
+            var videoRequest = request as VideoRequestDto;
 
             for (var i = 0; i < vals.Length; i++)
             {
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 4605c01831..c84135085f 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Helpers
                 job.BitRate = bitRate;
             }
 
-            var deviceId = state.DeviceId;
+            var deviceId = state.Request.DeviceId;
 
             if (!string.IsNullOrWhiteSpace(deviceId))
             {
@@ -486,7 +486,7 @@ namespace Jellyfin.Api.Helpers
             HttpRequest request,
             TranscodingJobType transcodingJobType,
             CancellationTokenSource cancellationTokenSource,
-            string workingDirectory = null)
+            string? workingDirectory = null)
         {
             Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
 
@@ -525,12 +525,12 @@ namespace Jellyfin.Api.Helpers
 
             var transcodingJob = this.OnTranscodeBeginning(
                 outputPath,
-                state.PlaySessionId,
+                state.Request.PlaySessionId,
                 state.MediaSource.LiveStreamId,
                 Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
                 transcodingJobType,
                 process,
-                state.DeviceId,
+                state.Request.DeviceId,
                 state,
                 cancellationTokenSource);
 
@@ -706,9 +706,9 @@ namespace Jellyfin.Api.Helpers
                 _transcodingLocks.Remove(path);
             }
 
-            if (!string.IsNullOrWhiteSpace(state.DeviceId))
+            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
             {
-                _sessionManager.ClearTranscodingInfo(state.DeviceId);
+                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
             }
         }
 
@@ -747,7 +747,7 @@ namespace Jellyfin.Api.Helpers
                 state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
-            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.LiveStreamId))
+            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
             {
                 var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
                     new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index db7cc6a75c..70a13d745f 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamState" /> class.
         /// </summary>
-        /// <param name="mediaSourceManager">Instance of the <see cref="mediaSourceManager" /> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param>
         /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param>
         public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper)
@@ -34,29 +34,28 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public string? RequestedUrl { get; set; }
 
-        // /// <summary>
-        // /// Gets or sets the request.
-        // /// </summary>
-        // public StreamRequest Request
-        // {
-        //     get => (StreamRequest)BaseRequest;
-        //     set
-        //     {
-        //         BaseRequest = value;
-        //
-        //         IsVideoRequest = VideoRequest != null;
-        //     }
-        // }
+        /// <summary>
+        /// Gets or sets the request.
+        /// </summary>
+        public StreamingRequestDto Request
+        {
+            get => (StreamingRequestDto)BaseRequest;
+            set
+            {
+                BaseRequest = value;
+                IsVideoRequest = VideoRequest != null;
+            }
+        }
 
         /// <summary>
         /// Gets or sets the transcoding throttler.
         /// </summary>
         public TranscodingThrottler? TranscodingThrottler { get; set; }
 
-        /*/// <summary>
+        /// <summary>
         /// Gets the video request.
         /// </summary>
-        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;*/
+        public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
 
         /// <summary>
         /// Gets or sets the direct stream provicer.
@@ -68,10 +67,10 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public string? WaitForPath { get; set; }
 
-        /*/// <summary>
+        /// <summary>
         /// Gets a value indicating whether the request outputs video.
         /// </summary>
-        public bool IsOutputVideo => Request is VideoStreamRequest;*/
+        public bool IsOutputVideo => Request is VideoRequestDto;
 
         /// <summary>
         /// Gets the segment length.
@@ -161,36 +160,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// </summary>
         public TranscodingJobDto? TranscodingJob { get; set; }
 
-        /// <summary>
-        /// Gets or sets the device id.
-        /// </summary>
-        public string? DeviceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the play session id.
-        /// </summary>
-        public string? PlaySessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the live stream id.
-        /// </summary>
-        public string? LiveStreamId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video coded.
-        /// </summary>
-        public string? VideoCodec { get; set; }
-
-        /// <summary>
-        /// Gets or sets the audio codec.
-        /// </summary>
-        public string? AudioCodec { get; set; }
-
-        /// <summary>
-        /// Gets or sets the subtitle codec.
-        /// </summary>
-        public string? SubtitleCodec { get; set; }
-
         /// <inheritdoc />
         public void Dispose()
         {
@@ -219,7 +188,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
             {
                 // REVIEW: Is this the right place for this?
                 if (MediaSource.RequiresClosing
-                    && string.IsNullOrWhiteSpace(LiveStreamId)
+                    && string.IsNullOrWhiteSpace(Request.LiveStreamId)
                     && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
                 {
                     _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
new file mode 100644
index 0000000000..1791b03706
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.MediaEncoding;
+
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+    /// <summary>
+    /// The audio streaming request dto.
+    /// </summary>
+    public class StreamingRequestDto : BaseEncodingJobOptions
+    {
+        /// <summary>
+        /// Gets or sets the device profile.
+        /// </summary>
+        public string? DeviceProfileId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the params.
+        /// </summary>
+        public string? Params { get; set; }
+
+        /// <summary>
+        /// Gets or sets the play session id.
+        /// </summary>
+        public string? PlaySessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the tag.
+        /// </summary>
+        public string? Tag { get; set; }
+
+        /// <summary>
+        /// Gets or sets the segment container.
+        /// </summary>
+        public string? SegmentContainer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the segment length.
+        /// </summary>
+        public int? SegmentLength { get; set; }
+
+        /// <summary>
+        /// Gets or sets the min segments.
+        /// </summary>
+        public int? MinSegments { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
new file mode 100644
index 0000000000..cce2a89d49
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+    /// <summary>
+    /// The video request dto.
+    /// </summary>
+    public class VideoRequestDto : StreamingRequestDto
+    {
+        /// <summary>
+        /// Gets a value indicating whether this instance has fixed resolution.
+        /// </summary>
+        /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
+        public bool HasFixedResolution => Width.HasValue || Height.HasValue;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to enable subtitles in the manifest.
+        /// </summary>
+        public bool EnableSubtitlesInManifest { get; set; }
+    }
+}

From 1cb20f91814cacdbb1866f42450cab9ae8000958 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 19:44:17 +0200
Subject: [PATCH 332/463] Fix build

---
 Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 71bf053f58..caa601cf31 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -194,7 +194,9 @@ namespace Jellyfin.Api.Helpers
 
             if (string.IsNullOrEmpty(containerInternal))
             {
-                containerInternal = (streamingRequest.Static && streamingRequest.Static) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
+                containerInternal = streamingRequest.Static ?
+                    StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
+                    : GetOutputFileExtension(state);
             }
 
             state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');

From cff9772e147bcf31e19dd12def0691692ad663a5 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 22 Jul 2020 20:13:51 +0200
Subject: [PATCH 333/463] Fix build part 2

---
 Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index caa601cf31..0b18756d6c 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -185,7 +185,7 @@ namespace Jellyfin.Api.Helpers
 
             encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
 
-            var containerInternal = Path.GetExtension(state.RequestedUrl);
+            string? containerInternal = Path.GetExtension(state.RequestedUrl);
 
             if (string.IsNullOrEmpty(streamingRequest.Container))
             {

From 9f323e55791b6705f78b552d141e3362d967df08 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Wed, 22 Jul 2020 14:37:17 -0400
Subject: [PATCH 334/463] Add missing chromecast version
 serialization/deserialization.

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs    | 8 +++++++-
 MediaBrowser.Api/DisplayPreferencesService.cs             | 2 ++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 1ed23fe8e4..447d74070f 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -68,6 +68,11 @@ namespace Jellyfin.Server.Migrations.Routines
                 foreach (var result in results)
                 {
                     var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+                    var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
+                        ? Enum.TryParse<ChromecastVersion>(version, true, out var parsed)
+                            ? parsed
+                            : ChromecastVersion.Stable
+                        : ChromecastVersion.Stable;
 
                     var displayPreferences = new DisplayPreferences(result[2].ToString(), new Guid(result[1].ToBlob()))
                     {
@@ -79,7 +84,8 @@ namespace Jellyfin.Server.Migrations.Routines
                         SortOrder = dto.SortOrder,
                         RememberIndexing = dto.RememberIndexing,
                         RememberSorting = dto.RememberSorting,
-                        ScrollDirection = dto.ScrollDirection
+                        ScrollDirection = dto.ScrollDirection,
+                        ChromecastVersion = chromecastVersion
                     };
 
                     for (int i = 0; i < 7; i++)
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 877b124be5..b95ab0dfde 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -103,6 +103,8 @@ namespace MediaBrowser.Api
                 dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
             }
 
+            dto.CustomPrefs["chromecastVersion"] = result.ChromecastVersion.ToString().ToLowerInvariant();
+
             return ToOptimizedResult(dto);
         }
 

From 52ebf6ae8f050fcbdf675529a97cfbbfcca4953a Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Wed, 22 Jul 2020 14:53:32 -0400
Subject: [PATCH 335/463] Move DisplayPreferencesManager.cs to Users namespace

---
 .../{ => Users}/DisplayPreferencesManager.cs                    | 2 +-
 MediaBrowser.Api/DisplayPreferencesService.cs                   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
 rename Jellyfin.Server.Implementations/{ => Users}/DisplayPreferencesManager.cs (96%)

diff --git a/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
similarity index 96%
rename from Jellyfin.Server.Implementations/DisplayPreferencesManager.cs
rename to Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 132e74c6aa..29ec6e706e 100644
--- a/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -3,7 +3,7 @@ using System.Linq;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller;
 
-namespace Jellyfin.Server.Implementations
+namespace Jellyfin.Server.Implementations.Users
 {
     /// <summary>
     /// Manages the storage and retrieval of display preferences through Entity Framework.
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index b95ab0dfde..cca2092e97 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -148,7 +148,7 @@ namespace MediaBrowser.Api
 
             foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))
             {
-                var order = int.Parse(key.Substring("homesection".Length));
+                var order = int.Parse(key.AsSpan().Slice("homesection".Length));
                 if (!Enum.TryParse<HomeSectionType>(request.CustomPrefs[key], true, out var type))
                 {
                     type = order < 7 ? defaults[order] : HomeSectionType.None;

From 8a9ec7809fab05a30ff8e5f13b38b9d34ed15050 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Wed, 22 Jul 2020 15:19:18 -0400
Subject: [PATCH 336/463] Wrap context creation with using

---
 .../Users/DisplayPreferencesManager.cs                       | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 29ec6e706e..4ad9a12d42 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -2,6 +2,7 @@
 using System.Linq;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller;
+using Microsoft.EntityFrameworkCore;
 
 namespace Jellyfin.Server.Implementations.Users
 {
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             var user = dbContext.Users.Find(userId);
 #pragma warning disable CA1307
             var prefs = user.DisplayPreferences.FirstOrDefault(pref => string.Equals(pref.Client, client));
@@ -41,7 +42,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public void SaveChanges(DisplayPreferences preferences)
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             dbContext.Update(preferences);
             dbContext.SaveChanges();
         }

From 5f67ba4d7087d7a0cad37fc9e2db212340076d12 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Wed, 22 Jul 2020 15:21:50 -0400
Subject: [PATCH 337/463] Restructure query to avoid extra database access.

---
 .../Users/DisplayPreferencesManager.cs              | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 4ad9a12d42..b7c65fc2cb 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -1,4 +1,6 @@
-using System;
+#pragma warning disable CA1307
+
+using System;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller;
@@ -26,14 +28,15 @@ namespace Jellyfin.Server.Implementations.Users
         public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
         {
             using var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users.Find(userId);
-#pragma warning disable CA1307
-            var prefs = user.DisplayPreferences.FirstOrDefault(pref => string.Equals(pref.Client, client));
+            var prefs = dbContext.DisplayPreferences
+                .Include(pref => pref.HomeSections)
+                .FirstOrDefault(pref =>
+                    pref.UserId == userId && pref.ItemId == null && string.Equals(pref.Client, client));
 
             if (prefs == null)
             {
                 prefs = new DisplayPreferences(client, userId);
-                user.DisplayPreferences.Add(prefs);
+                dbContext.DisplayPreferences.Add(prefs);
             }
 
             return prefs;

From d39f481a5c723dcbd97a578dc8f390e7d0b4e984 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 23 Jul 2020 12:46:54 +0200
Subject: [PATCH 338/463] Apply suggestions from review

---
 Jellyfin.Api/Controllers/AudioController.cs      | 16 +++++++---------
 .../Helpers/FileStreamResponseHelpers.cs         |  6 +++---
 Jellyfin.Api/Models/StreamingDtos/StreamState.cs |  5 -----
 3 files changed, 10 insertions(+), 17 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 81492ed4aa..d8c67cc24a 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
@@ -40,6 +41,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly HttpClient _httpClient;
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
@@ -59,6 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+        /// <param name="httpClient">Instance of the <see cref="HttpClient"/>.</param>
         public AudioController(
             IDlnaManager dlnaManager,
             IUserManager userManger,
@@ -72,7 +75,8 @@ namespace Jellyfin.Api.Controllers
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
             IDeviceManager deviceManager,
-            TranscodingJobHelper transcodingJobHelper)
+            TranscodingJobHelper transcodingJobHelper,
+            HttpClient httpClient)
         {
             _dlnaManager = dlnaManager;
             _authContext = authorizationContext;
@@ -87,6 +91,7 @@ namespace Jellyfin.Api.Controllers
             _configuration = configuration;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
+            _httpClient = httpClient;
         }
 
         /// <summary>
@@ -295,7 +300,7 @@ namespace Jellyfin.Api.Controllers
 
                 using (state)
                 {
-                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this).ConfigureAwait(false);
+                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
                 }
             }
 
@@ -327,13 +332,6 @@ namespace Jellyfin.Api.Controllers
                         return File(Response.Body, contentType);
                     }
 
-                    TimeSpan? cacheDuration = null;
-
-                    if (!string.IsNullOrEmpty(tag))
-                    {
-                        cacheDuration = TimeSpan.FromDays(365);
-                    }
-
                     return FileStreamResponseHelpers.GetStaticFileResult(
                         state.MediaPath,
                         contentType,
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 9f16b53236..ddca2f1ae6 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -23,14 +23,14 @@ namespace Jellyfin.Api.Helpers
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
+        /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
         /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
         public static async Task<ActionResult> GetStaticRemoteStreamResult(
             StreamState state,
             bool isHeadRequest,
-            ControllerBase controller)
+            ControllerBase controller,
+            HttpClient httpClient)
         {
-            HttpClient httpClient = new HttpClient();
-
             if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
             {
                 httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index 70a13d745f..df5e21dac0 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -94,11 +94,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
                         userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
                         userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
                     {
-                        if (IsSegmentedLiveStream)
-                        {
-                            return 6;
-                        }
-
                         return 6;
                     }
 

From 629ffe395feccbf512709bfaa54d5ef6d23c6852 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Thu, 23 Jul 2020 20:36:36 -0400
Subject: [PATCH 339/463] Fixed build errors.

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs         | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 447d74070f..588c0ecdde 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -45,6 +45,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc />
         public string Name => "MigrateDisplayPreferencesDatabase";
 
+        /// <inheritdoc />
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc />
         public void Perform()
         {

From ca3dcc3db03d531457b4b60cc3ecdebd57a0157e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 24 Jul 2020 19:14:53 +0200
Subject: [PATCH 340/463] Fix suggestions from review

---
 Jellyfin.Api/Controllers/AudioController.cs   | 107 +++++-------------
 .../Helpers/FileStreamResponseHelpers.cs      |  17 +--
 Jellyfin.Api/Helpers/StreamingHelpers.cs      |  68 ++++-------
 .../Models/StreamingDtos/StreamState.cs       |  10 +-
 4 files changed, 58 insertions(+), 144 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index d8c67cc24a..7405c26fb8 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -260,7 +260,7 @@ namespace Jellyfin.Api.Controllers
                 StreamOptions = streamOptions
             };
 
-            var state = await StreamingHelpers.GetStreamingState(
+            using var state = await StreamingHelpers.GetStreamingState(
                     streamingRequest,
                     Request,
                     _authContext,
@@ -283,14 +283,11 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                using (state)
-                {
-                    // TODO AllowEndOfFile = false
-                    await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                // TODO AllowEndOfFile = false
+                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
 
-                    // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
-                    return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
-                }
+                // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+                return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
             }
 
             // Static remote stream
@@ -298,10 +295,7 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                using (state)
-                {
-                    return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
-                }
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
             }
 
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@@ -322,80 +316,35 @@ namespace Jellyfin.Api.Controllers
             {
                 var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
 
-                using (state)
+                if (state.MediaSource.IsInfiniteStream)
                 {
-                    if (state.MediaSource.IsInfiniteStream)
-                    {
-                        // TODO AllowEndOfFile = false
-                        await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
-
-                        return File(Response.Body, contentType);
-                    }
+                    // TODO AllowEndOfFile = false
+                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
 
-                    return FileStreamResponseHelpers.GetStaticFileResult(
-                        state.MediaPath,
-                        contentType,
-                        isHeadRequest,
-                        this);
+                    return File(Response.Body, contentType);
                 }
-            }
 
-            /*
-            // Not static but transcode cache file exists
-            if (isTranscodeCached && state.VideoRequest == null)
-            {
-                var contentType = state.GetMimeType(outputPath)
-                try
-                {
-                    if (transcodingJob != null)
-                    {
-                        ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
-                    }
-                    return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-                    {
-                        ResponseHeaders = responseHeaders,
-                        ContentType = contentType,
-                        IsHeadRequest = isHeadRequest,
-                        Path = outputPath,
-                        FileShare = FileShare.ReadWrite,
-                        OnComplete = () =>
-                        {
-                            if (transcodingJob != null)
-                            {
-                                ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
-                            }
-                    }).ConfigureAwait(false);
-                }
-                finally
-                {
-                    state.Dispose();
-                }
-            }
-            */
-
-            // Need to start ffmpeg (because media can't be returned directly)
-            try
-            {
-                var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
-                var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
-                var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
-                return await FileStreamResponseHelpers.GetTranscodedFile(
-                    state,
+                return FileStreamResponseHelpers.GetStaticFileResult(
+                    state.MediaPath,
+                    contentType,
                     isHeadRequest,
-                    _streamHelper,
-                    this,
-                    _transcodingJobHelper,
-                    ffmpegCommandLineArguments,
-                    Request,
-                    _transcodingJobType,
-                    cancellationTokenSource).ConfigureAwait(false);
+                    this);
             }
-            catch
-            {
-                state.Dispose();
 
-                throw;
-            }
+            // Need to start ffmpeg (because media can't be returned directly)
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+            return await FileStreamResponseHelpers.GetTranscodedFile(
+                state,
+                isHeadRequest,
+                _streamHelper,
+                this,
+                _transcodingJobHelper,
+                ffmpegCommandLineArguments,
+                Request,
+                _transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
         }
     }
 }
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index ddca2f1ae6..636f47f5f1 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -36,17 +36,14 @@ namespace Jellyfin.Api.Helpers
                 httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
             }
 
-            var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
+            using var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
             var contentType = response.Content.Headers.ContentType.ToString();
 
             controller.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
             if (isHeadRequest)
             {
-                using (response)
-                {
-                    return controller.File(Array.Empty<byte>(), contentType);
-                }
+                return controller.File(Array.Empty<byte>(), contentType);
             }
 
             return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
@@ -74,7 +71,7 @@ namespace Jellyfin.Api.Helpers
                 return controller.NoContent();
             }
 
-            var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
+            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
             return controller.File(stream, contentType);
         }
 
@@ -129,11 +126,9 @@ namespace Jellyfin.Api.Helpers
                     state.Dispose();
                 }
 
-                using (var memoryStream = new MemoryStream())
-                {
-                    await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
-                    return controller.File(memoryStream, contentType);
-                }
+                await using var memoryStream = new MemoryStream();
+                await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                return controller.File(memoryStream, contentType);
             }
             finally
             {
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 0b18756d6c..b12590080c 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Helpers
             {
                 var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
 
-                streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
+                streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
             }
 
             if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
@@ -108,31 +108,22 @@ namespace Jellyfin.Api.Helpers
                 state.User = userManager.GetUserById(auth.UserId);
             }
 
-            /*
-            if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
-                (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
-                (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                state.SegmentLength = 6;
-            }
-            */
-
             if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
             {
-                state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
                 state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
             }
 
             if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
             {
-                state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
                 state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
                                            ?? state.SupportedAudioCodecs.FirstOrDefault();
             }
 
             if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
             {
-                state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
                 state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
                                               ?? state.SupportedSubtitleCodecs.FirstOrDefault();
             }
@@ -141,15 +132,6 @@ namespace Jellyfin.Api.Helpers
 
             state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
 
-            /*
-            var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
-                         item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
-            if (primaryImage != null)
-            {
-                state.AlbumCoverPath = primaryImage.Path;
-            }
-            */
-
             MediaSourceInfo? mediaSource = null;
             if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
             {
@@ -322,25 +304,24 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="value">The time seek header string.</param>
         /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
-        private static long? ParseTimeSeekHeader(string value)
+        private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
         {
-            if (string.IsNullOrWhiteSpace(value))
+            if (value.IsEmpty)
             {
                 return null;
             }
 
-            const string Npt = "npt=";
-            if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
+            const string npt = "npt=";
+            if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
             {
                 throw new ArgumentException("Invalid timeseek header");
             }
 
-            int index = value.IndexOf('-', StringComparison.InvariantCulture);
+            var index = value.IndexOf('-');
             value = index == -1
-                ? value.Substring(Npt.Length)
-                : value.Substring(Npt.Length, index - Npt.Length);
-
-            if (value.IndexOf(':', StringComparison.InvariantCulture) == -1)
+                ? value.Slice(npt.Length)
+                : value.Slice(npt.Length, index - npt.Length);
+            if (value.IndexOf(':') == -1)
             {
                 // Parses npt times in the format of '417.33'
                 if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
@@ -351,26 +332,15 @@ namespace Jellyfin.Api.Helpers
                 throw new ArgumentException("Invalid timeseek header");
             }
 
-            // Parses npt times in the format of '10:19:25.7'
-            var tokens = value.Split(new[] { ':' }, 3);
-            double secondsSum = 0;
-            var timeFactor = 3600;
-
-            foreach (var time in tokens)
+            try
             {
-                if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
-                {
-                    secondsSum += digit * timeFactor;
-                }
-                else
-                {
-                    throw new ArgumentException("Invalid timeseek header");
-                }
-
-                timeFactor /= 60;
+                // Parses npt times in the format of '10:19:25.7'
+                return TimeSpan.Parse(value).Ticks;
+            }
+            catch
+            {
+                throw new ArgumentException("Invalid timeseek header");
             }
-
-            return TimeSpan.FromSeconds(secondsSum).Ticks;
         }
 
         /// <summary>
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index df5e21dac0..e95f2d1f43 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -88,11 +88,11 @@ namespace Jellyfin.Api.Models.StreamingDtos
                 {
                     var userAgent = UserAgent ?? string.Empty;
 
-                    if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
+                    if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1
+                        || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1
+                        || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1
+                        || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1
+                        || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
                     {
                         return 6;
                     }

From 0d13d830bb3ae9fc0098aa802c16e428f6929341 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 24 Jul 2020 16:30:54 -0400
Subject: [PATCH 341/463] Migrate skip lengths.

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs   | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 588c0ecdde..f46fb7e89c 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Globalization;
 using System.IO;
 using System.Text.Json;
 using System.Text.Json.Serialization;
@@ -88,7 +89,13 @@ namespace Jellyfin.Server.Migrations.Routines
                         RememberIndexing = dto.RememberIndexing,
                         RememberSorting = dto.RememberSorting,
                         ScrollDirection = dto.ScrollDirection,
-                        ChromecastVersion = chromecastVersion
+                        ChromecastVersion = chromecastVersion,
+                        SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
+                            ? int.Parse(length, CultureInfo.InvariantCulture)
+                            : 30000,
+                        SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
+                            ? int.Parse(length, CultureInfo.InvariantCulture)
+                            : 30000
                     };
 
                     for (int i = 0; i < 7; i++)

From ff7105982a291c6823aaae90adc43d5f0e82ed98 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 24 Jul 2020 16:32:24 -0400
Subject: [PATCH 342/463] Read skip lengths from server.

---
 MediaBrowser.Api/DisplayPreferencesService.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index cca2092e97..0323284228 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -104,6 +104,8 @@ namespace MediaBrowser.Api
             }
 
             dto.CustomPrefs["chromecastVersion"] = result.ChromecastVersion.ToString().ToLowerInvariant();
+            dto.CustomPrefs["skipForwardLength"] = result.SkipForwardLength.ToString();
+            dto.CustomPrefs["skipBackLength"] = result.SkipBackwardLength.ToString();
 
             return ToOptimizedResult(dto);
         }

From 9fcf23bd213c311b47dcc4d5c124040b6bdbbc52 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 24 Jul 2020 16:34:19 -0400
Subject: [PATCH 343/463] Migrate EnableNextVideoInfoOverlay

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs       | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index f46fb7e89c..d8b081bb29 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -95,7 +95,10 @@ namespace Jellyfin.Server.Migrations.Routines
                             : 30000,
                         SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
                             ? int.Parse(length, CultureInfo.InvariantCulture)
-                            : 30000
+                            : 30000,
+                        EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
+                            ? bool.Parse(enabled)
+                            : true
                     };
 
                     for (int i = 0; i < 7; i++)

From 13d919f23649bf159ff44fb3f5b4e132549cbbf7 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 24 Jul 2020 16:35:20 -0400
Subject: [PATCH 344/463] Read EnableNextVideoInfoOverlay from database.

---
 MediaBrowser.Api/DisplayPreferencesService.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 0323284228..0f3427e541 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -106,6 +106,7 @@ namespace MediaBrowser.Api
             dto.CustomPrefs["chromecastVersion"] = result.ChromecastVersion.ToString().ToLowerInvariant();
             dto.CustomPrefs["skipForwardLength"] = result.SkipForwardLength.ToString();
             dto.CustomPrefs["skipBackLength"] = result.SkipBackwardLength.ToString();
+            dto.CustomPrefs["enableNextVideoInfoOverlay"] = result.EnableNextVideoInfoOverlay.ToString();
 
             return ToOptimizedResult(dto);
         }

From 37496e958f08d2f81976dc976b6d5de648de958f Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 24 Jul 2020 16:49:43 -0600
Subject: [PATCH 345/463] move VideoService.cs to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AudioController.cs   |  22 +-
 Jellyfin.Api/Controllers/VideosController.cs  | 318 +++++++++++++++++-
 .../Playback/Progressive/VideoService.cs      |  43 ---
 3 files changed, 328 insertions(+), 55 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 7405c26fb8..6e3d898bfa 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -205,13 +205,13 @@ namespace Jellyfin.Api.Controllers
         {
             bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
 
-            var cancellationTokenSource = new CancellationTokenSource();
+            using var cancellationTokenSource = new CancellationTokenSource();
 
             StreamingRequestDto streamingRequest = new StreamingRequestDto
             {
                 Id = itemId,
                 Container = container,
-                Static = @static.HasValue ? @static.Value : true,
+                Static = @static ?? true,
                 Params = @params,
                 Tag = tag,
                 DeviceProfileId = deviceProfileId,
@@ -222,10 +222,10 @@ namespace Jellyfin.Api.Controllers
                 MediaSourceId = mediaSourceId,
                 DeviceId = deviceId,
                 AudioCodec = audioCodec,
-                EnableAutoStreamCopy = enableAutoStreamCopy.HasValue ? enableAutoStreamCopy.Value : true,
-                AllowAudioStreamCopy = allowAudioStreamCopy.HasValue ? allowAudioStreamCopy.Value : true,
-                AllowVideoStreamCopy = allowVideoStreamCopy.HasValue ? allowVideoStreamCopy.Value : true,
-                BreakOnNonKeyFrames = breakOnNonKeyFrames.HasValue ? breakOnNonKeyFrames.Value : false,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
                 AudioSampleRate = audioSampleRate,
                 MaxAudioChannels = maxAudioChannels,
                 AudioBitRate = audioBitRate,
@@ -235,7 +235,7 @@ namespace Jellyfin.Api.Controllers
                 Level = level,
                 Framerate = framerate,
                 MaxFramerate = maxFramerate,
-                CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true,
+                CopyTimestamps = copyTimestamps ?? true,
                 StartTimeTicks = startTimeTicks,
                 Width = width,
                 Height = height,
@@ -244,13 +244,13 @@ namespace Jellyfin.Api.Controllers
                 SubtitleMethod = subtitleMethod,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
-                RequireAvc = requireAvc.HasValue ? requireAvc.Value : true,
-                DeInterlace = deInterlace.HasValue ? deInterlace.Value : true,
-                RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
                 TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
                 CpuCoreLimit = cpuCoreLimit,
                 LiveStreamId = liveStreamId,
-                EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
                 VideoCodec = videoCodec,
                 SubtitleCodec = subtitleCodec,
                 TranscodeReasons = transcodingReasons,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index e2a44427b8..734198d9aa 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -1,19 +1,34 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net.Http;
 using System.Threading;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -26,6 +41,20 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDtoService _dtoService;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IStreamHelper _streamHelper;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly HttpClient _httpClient;
+
+        private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="VideosController"/> class.
@@ -33,14 +62,50 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+        /// <param name="httpClient">Instance of the <see cref="HttpClient"/> class.</param>
         public VideosController(
             ILibraryManager libraryManager,
             IUserManager userManager,
-            IDtoService dtoService)
+            IDtoService dtoService,
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IStreamHelper streamHelper,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            HttpClient httpClient)
         {
             _libraryManager = libraryManager;
             _userManager = userManager;
             _dtoService = dtoService;
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _streamHelper = streamHelper;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _httpClient = httpClient;
         }
 
         /// <summary>
@@ -200,5 +265,256 @@ namespace Jellyfin.Api.Controllers
             primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
             return NoContent();
         }
+
+        /// <summary>
+        /// Gets a video stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("{itemId}/{stream=stream}.{container?}")]
+        [HttpGet("{itemId}/stream")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}")]
+        [HttpHead("{itemId}/stream")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetVideoStream(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+            using var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new StreamingRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+                // TODO AllowEndOfFile = false
+                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+
+                // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+                return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
+            }
+
+            // Static remote stream
+            if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
+            }
+
+            if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
+            {
+                return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = System.IO.File.Exists(outputPath);
+
+            var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
+
+            // Static stream
+            if (@static.HasValue && @static.Value)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                if (state.MediaSource.IsInfiniteStream)
+                {
+                    // TODO AllowEndOfFile = false
+                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+
+                    return File(Response.Body, contentType);
+                }
+
+                return FileStreamResponseHelpers.GetStaticFileResult(
+                    state.MediaPath,
+                    contentType,
+                    isHeadRequest,
+                    this);
+            }
+
+            // Need to start ffmpeg (because media can't be returned directly)
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+            var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+            return await FileStreamResponseHelpers.GetTranscodedFile(
+                state,
+                isHeadRequest,
+                _streamHelper,
+                this,
+                _transcodingJobHelper,
+                ffmpegCommandLineArguments,
+                Request,
+                _transcodingJobType,
+                cancellationTokenSource).ConfigureAwait(false);
+        }
     }
 }
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
index c3f6b905cb..5bc85f42d2 100644
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
@@ -14,49 +14,6 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
-    /// <summary>
-    /// Class GetVideoStream.
-    /// </summary>
-    [Route("/Videos/{Id}/stream.mpegts", "GET")]
-    [Route("/Videos/{Id}/stream.ts", "GET")]
-    [Route("/Videos/{Id}/stream.webm", "GET")]
-    [Route("/Videos/{Id}/stream.asf", "GET")]
-    [Route("/Videos/{Id}/stream.wmv", "GET")]
-    [Route("/Videos/{Id}/stream.ogv", "GET")]
-    [Route("/Videos/{Id}/stream.mp4", "GET")]
-    [Route("/Videos/{Id}/stream.m4v", "GET")]
-    [Route("/Videos/{Id}/stream.mkv", "GET")]
-    [Route("/Videos/{Id}/stream.mpeg", "GET")]
-    [Route("/Videos/{Id}/stream.mpg", "GET")]
-    [Route("/Videos/{Id}/stream.avi", "GET")]
-    [Route("/Videos/{Id}/stream.m2ts", "GET")]
-    [Route("/Videos/{Id}/stream.3gp", "GET")]
-    [Route("/Videos/{Id}/stream.wmv", "GET")]
-    [Route("/Videos/{Id}/stream.wtv", "GET")]
-    [Route("/Videos/{Id}/stream.mov", "GET")]
-    [Route("/Videos/{Id}/stream.iso", "GET")]
-    [Route("/Videos/{Id}/stream.flv", "GET")]
-    [Route("/Videos/{Id}/stream.rm", "GET")]
-    [Route("/Videos/{Id}/stream", "GET")]
-    [Route("/Videos/{Id}/stream.ts", "HEAD")]
-    [Route("/Videos/{Id}/stream.webm", "HEAD")]
-    [Route("/Videos/{Id}/stream.asf", "HEAD")]
-    [Route("/Videos/{Id}/stream.wmv", "HEAD")]
-    [Route("/Videos/{Id}/stream.ogv", "HEAD")]
-    [Route("/Videos/{Id}/stream.mp4", "HEAD")]
-    [Route("/Videos/{Id}/stream.m4v", "HEAD")]
-    [Route("/Videos/{Id}/stream.mkv", "HEAD")]
-    [Route("/Videos/{Id}/stream.mpeg", "HEAD")]
-    [Route("/Videos/{Id}/stream.mpg", "HEAD")]
-    [Route("/Videos/{Id}/stream.avi", "HEAD")]
-    [Route("/Videos/{Id}/stream.3gp", "HEAD")]
-    [Route("/Videos/{Id}/stream.wmv", "HEAD")]
-    [Route("/Videos/{Id}/stream.wtv", "HEAD")]
-    [Route("/Videos/{Id}/stream.m2ts", "HEAD")]
-    [Route("/Videos/{Id}/stream.mov", "HEAD")]
-    [Route("/Videos/{Id}/stream.iso", "HEAD")]
-    [Route("/Videos/{Id}/stream.flv", "HEAD")]
-    [Route("/Videos/{Id}/stream", "HEAD")]
     public class GetVideoStream : VideoStreamRequest
     {
     }

From d801621cfc902f1c65f3da8d466d625e1d7630b1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 24 Jul 2020 17:22:32 -0600
Subject: [PATCH 346/463] Fix request parameters

---
 .../Controllers/LibraryStructureController.cs | 21 +++++----------
 .../LibraryStructureDto/MediaPathDto.cs       | 27 +++++++++++++++++++
 2 files changed, 34 insertions(+), 14 deletions(-)
 create mode 100644 Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs

diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index d3537a7a70..88ae752aed 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.LibraryStructureDto;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
@@ -16,6 +17,7 @@ using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -74,7 +76,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? name,
             [FromQuery] string? collectionType,
             [FromQuery] string[] paths,
-            [FromQuery] LibraryOptions? libraryOptions,
+            [FromBody] LibraryOptions? libraryOptions,
             [FromQuery] bool refreshLibrary = false)
         {
             libraryOptions ??= new LibraryOptions();
@@ -194,9 +196,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Add a media path to a library.
         /// </summary>
-        /// <param name="name">The name of the library.</param>
-        /// <param name="path">The path to add.</param>
-        /// <param name="pathInfo">The path info.</param>
+        /// <param name="mediaPathDto">The media path dto.</param>
         /// <param name="refreshLibrary">Whether to refresh the library.</param>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <response code="204">Media path added.</response>
@@ -204,23 +204,16 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Paths")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddMediaPath(
-            [FromQuery] string? name,
-            [FromQuery] string? path,
-            [FromQuery] MediaPathInfo? pathInfo,
+            [FromBody, BindRequired] MediaPathDto mediaPathDto,
             [FromQuery] bool refreshLibrary = false)
         {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
             _libraryMonitor.Stop();
 
             try
             {
-                var mediaPath = pathInfo ?? new MediaPathInfo { Path = path };
+                var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
 
-                _libraryManager.AddMediaPath(name, mediaPath);
+                _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
             }
             finally
             {
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
new file mode 100644
index 0000000000..f659882595
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+    /// <summary>
+    /// Media Path dto.
+    /// </summary>
+    public class MediaPathDto
+    {
+        /// <summary>
+        /// Gets or sets the name of the library.
+        /// </summary>
+        [Required]
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path to add.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path info.
+        /// </summary>
+        public MediaPathInfo? PathInfo { get; set; }
+    }
+}
\ No newline at end of file

From 5924a81eebe8712bf140dd663272cc2aa23849b9 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 24 Jul 2020 17:31:11 -0600
Subject: [PATCH 347/463] Fix request parameters

---
 .../LibraryStructureDto/LibraryOptionsDto.cs      | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs

diff --git a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs
new file mode 100644
index 0000000000..a13cb90dbe
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+    /// <summary>
+    /// Library options dto.
+    /// </summary>
+    public class LibraryOptionsDto
+    {
+        /// <summary>
+        /// Gets or sets library options.
+        /// </summary>
+        public LibraryOptions? LibraryOptions { get; set; }
+    }
+}
\ No newline at end of file

From 259b5d7f0a051cb835839a90f7d9fd223cc12456 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 24 Jul 2020 17:46:17 -0600
Subject: [PATCH 348/463] Fix request parameters

---
 Jellyfin.Api/Controllers/LibraryStructureController.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 88ae752aed..b7f3c9b07c 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="name">The name of the virtual folder.</param>
         /// <param name="collectionType">The type of the collection.</param>
         /// <param name="paths">The paths of the virtual folder.</param>
-        /// <param name="libraryOptions">The library options.</param>
+        /// <param name="libraryOptionsDto">The library options.</param>
         /// <param name="refreshLibrary">Whether to refresh the library.</param>
         /// <response code="204">Folder added.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
@@ -76,10 +76,10 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? name,
             [FromQuery] string? collectionType,
             [FromQuery] string[] paths,
-            [FromBody] LibraryOptions? libraryOptions,
+            [FromBody] LibraryOptionsDto? libraryOptionsDto,
             [FromQuery] bool refreshLibrary = false)
         {
-            libraryOptions ??= new LibraryOptions();
+            var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
 
             if (paths != null && paths.Length > 0)
             {

From 7bb34fc9e7e480e7048a1e15e1f463afab2198eb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 25 Jul 2020 17:21:40 -0600
Subject: [PATCH 349/463] use proper HttpClient DI

---
 Jellyfin.Api/Controllers/AudioController.cs  | 11 ++++++-----
 Jellyfin.Api/Controllers/VideosController.cs | 11 ++++++-----
 Jellyfin.Api/Jellyfin.Api.csproj             |  1 +
 Jellyfin.Server/Startup.cs                   |  2 ++
 4 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 6e3d898bfa..86577411f2 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -41,7 +41,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly HttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
-        /// <param name="httpClient">Instance of the <see cref="HttpClient"/>.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         public AudioController(
             IDlnaManager dlnaManager,
             IUserManager userManger,
@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
             IConfiguration configuration,
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
-            HttpClient httpClient)
+            IHttpClientFactory httpClientFactory)
         {
             _dlnaManager = dlnaManager;
             _authContext = authorizationContext;
@@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers
             _configuration = configuration;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         /// <summary>
@@ -295,7 +295,8 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
+                using var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
             }
 
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 734198d9aa..5050c3d4fe 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly HttpClient _httpClient;
+        private readonly IHttpClientFactory _httpClientFactory;
 
         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
 
@@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
-        /// <param name="httpClient">Instance of the <see cref="HttpClient"/> class.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         public VideosController(
             ILibraryManager libraryManager,
             IUserManager userManager,
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
             IConfiguration configuration,
             IDeviceManager deviceManager,
             TranscodingJobHelper transcodingJobHelper,
-            HttpClient httpClient)
+            IHttpClientFactory httpClientFactory)
         {
             _libraryManager = libraryManager;
             _userManager = userManager;
@@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers
             _configuration = configuration;
             _deviceManager = deviceManager;
             _transcodingJobHelper = transcodingJobHelper;
-            _httpClient = httpClient;
+            _httpClientFactory = httpClientFactory;
         }
 
         /// <summary>
@@ -465,7 +465,8 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
+                using var httpClient = _httpClientFactory.CreateClient();
+                return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
             }
 
             if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 572cb1af25..a52b234d48 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -16,6 +16,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
   </ItemGroup>
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index edf023fa24..108d8f881e 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,3 +1,4 @@
+using System.Net.Http;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Models;
@@ -43,6 +44,7 @@ namespace Jellyfin.Server
             services.AddCustomAuthentication();
 
             services.AddJellyfinApiAuthorization();
+            services.AddHttpClient();
         }
 
         /// <summary>

From 4aa0bd064feca0e2e22189051b1bba886ddad215 Mon Sep 17 00:00:00 2001
From: David Ullmer <daullmer@gmail.com>
Date: Mon, 27 Jul 2020 09:47:19 +0200
Subject: [PATCH 350/463] Move HlsSegmentService to Jellyfin.Api

---
 .../Controllers/HlsSegmentController.cs       | 153 ++++++++++++++++
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |  14 ++
 .../Playback/Hls/HlsSegmentService.cs         | 164 ------------------
 3 files changed, 167 insertions(+), 164 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/HlsSegmentController.cs
 delete mode 100644 MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
new file mode 100644
index 0000000000..efdb6a3691
--- /dev/null
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The hls segment controller.
+    /// </summary>
+    public class HlsSegmentController : BaseJellyfinApiController
+    {
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
+        /// </summary>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
+        public HlsSegmentController(
+            IFileSystem fileSystem,
+            IServerConfigurationManager serverConfigurationManager,
+            TranscodingJobHelper transcodingJobHelper)
+        {
+            _fileSystem = fileSystem;
+            _serverConfigurationManager = serverConfigurationManager;
+            _transcodingJobHelper = transcodingJobHelper;
+        }
+
+        /// <summary>
+        /// Gets the specified audio segment for an audio item.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="segmentId">The segment id.</param>
+        /// <response code="200">Hls audio segment returned.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
+        // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
+        // [Authenticated]
+        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")]
+        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+        public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
+        {
+            // TODO: Deprecate with new iOS app
+            var file = segmentId + Path.GetExtension(Request.Path);
+            file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+
+            return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
+        }
+
+        /// <summary>
+        /// Gets a hls video playlist.
+        /// </summary>
+        /// <param name="itemId">The video id.</param>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <response code="200">Hls video playlist returned.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
+        [HttpGet("/Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+        public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId)
+        {
+            var file = playlistId + Path.GetExtension(Request.Path);
+            file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+
+            return GetFileResult(file, file);
+        }
+
+        /// <summary>
+        /// Stops an active encoding.
+        /// </summary>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <response code="204">Encoding stopped successfully.</response>
+        /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+        [HttpDelete("/Videos/ActiveEncodings")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
+        {
+            _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Gets a hls video segment.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="segmentId">The segment id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <response code="200">Hls video segment returned.</response>
+        /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
+        // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
+        // [Authenticated]
+        [HttpGet("/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+        public ActionResult GetHlsVideoSegmentLegacy(
+            [FromRoute] string itemId,
+            [FromRoute] string playlistId,
+            [FromRoute] string segmentId,
+            [FromRoute] string segmentContainer)
+        {
+            var file = segmentId + Path.GetExtension(Request.Path);
+            var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
+
+            file = Path.Combine(transcodeFolderPath, file);
+
+            var normalizedPlaylistId = playlistId;
+
+            var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
+                .FirstOrDefault(i =>
+                    string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
+                    && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
+
+            return GetFileResult(file, playlistPath);
+        }
+
+        private ActionResult GetFileResult(string path, string playlistPath)
+        {
+            var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+
+            Response.OnCompleted(() =>
+            {
+                if (transcodingJob != null)
+                {
+                    _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+                }
+
+                return Task.CompletedTask;
+            });
+
+            return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c84135085f..76f7c8fde0 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -712,6 +712,20 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        /// <summary>
+        /// Transcoding video finished. Decrement the active request counter.
+        /// </summary>
+        /// <param name="job">The <see cref="TranscodingJobDto"/> which ended.</param>
+        public void OnTranscodeEndRequest(TranscodingJobDto job)
+        {
+            job.ActiveRequestCount--;
+            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
+            if (job.ActiveRequestCount <= 0)
+            {
+                PingTimer(job, false);
+            }
+        }
+
         /// <summary>
         /// Processes the exited.
         /// </summary>
diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
deleted file mode 100644
index 8a3d00283f..0000000000
--- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
-    /// <summary>
-    /// Class GetHlsAudioSegment.
-    /// </summary>
-    // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
-    //[Authenticated]
-    [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
-    [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
-    public class GetHlsAudioSegmentLegacy
-    {
-        // TODO: Deprecate with new iOS app
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetHlsVideoSegment.
-    /// </summary>
-    [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
-    [Authenticated]
-    public class GetHlsPlaylistLegacy
-    {
-        // TODO: Deprecate with new iOS app
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public string Id { get; set; }
-
-        public string PlaylistId { get; set; }
-    }
-
-    [Route("/Videos/ActiveEncodings", "DELETE")]
-    [Authenticated]
-    public class StopEncodingProcess
-    {
-        [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string DeviceId { get; set; }
-
-        [ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
-        public string PlaySessionId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetHlsVideoSegment.
-    /// </summary>
-    // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
-    //[Authenticated]
-    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
-    public class GetHlsVideoSegmentLegacy : VideoStreamRequest
-    {
-        public string PlaylistId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
-    public class HlsSegmentService : BaseApiService
-    {
-        private readonly IFileSystem _fileSystem;
-
-        public HlsSegmentService(
-            ILogger<HlsSegmentService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IFileSystem fileSystem)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _fileSystem = fileSystem;
-        }
-
-        public Task<object> Get(GetHlsPlaylistLegacy request)
-        {
-            var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
-            file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
-            return GetFileResult(file, file);
-        }
-
-        public Task Delete(StopEncodingProcess request)
-        {
-            return ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetHlsVideoSegmentLegacy request)
-        {
-            var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
-            var transcodeFolderPath = ServerConfigurationManager.GetTranscodePath();
-
-            file = Path.Combine(transcodeFolderPath, file);
-
-            var normalizedPlaylistId = request.PlaylistId;
-
-            var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
-                .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
-
-            return GetFileResult(file, playlistPath);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetHlsAudioSegmentLegacy request)
-        {
-            // TODO: Deprecate with new iOS app
-            var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
-            file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
-            return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
-        }
-
-        private Task<object> GetFileResult(string path, string playlistPath)
-        {
-            var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-
-            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                Path = path,
-                FileShare = FileShare.ReadWrite,
-                OnComplete = () =>
-                {
-                    if (transcodingJob != null)
-                    {
-                        ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
-                    }
-                }
-            });
-        }
-    }
-}

From b8d327889b96b820249ddf80ee023b189f67f4a3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 27 Jul 2020 13:42:40 -0600
Subject: [PATCH 351/463] Add missing functions

---
 Jellyfin.Api/Controllers/AudioController.cs   |  19 +-
 Jellyfin.Api/Controllers/LiveTvController.cs  |  19 +-
 Jellyfin.Api/Controllers/VideosController.cs  |  21 +--
 .../Helpers/FileStreamResponseHelpers.cs      |  14 +-
 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 162 +++++++++++++++---
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |  14 ++
 6 files changed, 187 insertions(+), 62 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 86577411f2..e63868339f 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -35,7 +35,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IStreamHelper _streamHelper;
         private readonly IFileSystem _fileSystem;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IConfiguration _configuration;
@@ -55,7 +54,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
@@ -70,7 +68,6 @@ namespace Jellyfin.Api.Controllers
             IMediaSourceManager mediaSourceManager,
             IServerConfigurationManager serverConfigurationManager,
             IMediaEncoder mediaEncoder,
-            IStreamHelper streamHelper,
             IFileSystem fileSystem,
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
@@ -85,7 +82,6 @@ namespace Jellyfin.Api.Controllers
             _mediaSourceManager = mediaSourceManager;
             _serverConfigurationManager = serverConfigurationManager;
             _mediaEncoder = mediaEncoder;
-            _streamHelper = streamHelper;
             _fileSystem = fileSystem;
             _subtitleEncoder = subtitleEncoder;
             _configuration = configuration;
@@ -283,8 +279,11 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                // TODO AllowEndOfFile = false
-                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(Response.Body, CancellationToken.None)
+                    .ConfigureAwait(false);
 
                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@@ -319,8 +318,11 @@ namespace Jellyfin.Api.Controllers
 
                 if (state.MediaSource.IsInfiniteStream)
                 {
-                    // TODO AllowEndOfFile = false
-                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
 
                     return File(Response.Body, contentType);
                 }
@@ -339,7 +341,6 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 isHeadRequest,
-                _streamHelper,
                 this,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index bc5446510a..9144d6f285 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -24,7 +24,6 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
@@ -45,9 +44,9 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
         private readonly ISessionContext _sessionContext;
-        private readonly IStreamHelper _streamHelper;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IConfigurationManager _configurationManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -58,9 +57,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
         /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
         public LiveTvController(
             ILiveTvManager liveTvManager,
             IUserManager userManager,
@@ -68,9 +67,9 @@ namespace Jellyfin.Api.Controllers
             ILibraryManager libraryManager,
             IDtoService dtoService,
             ISessionContext sessionContext,
-            IStreamHelper streamHelper,
             IMediaSourceManager mediaSourceManager,
-            IConfigurationManager configurationManager)
+            IConfigurationManager configurationManager,
+            TranscodingJobHelper transcodingJobHelper)
         {
             _liveTvManager = liveTvManager;
             _userManager = userManager;
@@ -78,9 +77,9 @@ namespace Jellyfin.Api.Controllers
             _libraryManager = libraryManager;
             _dtoService = dtoService;
             _sessionContext = sessionContext;
-            _streamHelper = streamHelper;
             _mediaSourceManager = mediaSourceManager;
             _configurationManager = configurationManager;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
         /// <summary>
@@ -1187,7 +1186,9 @@ namespace Jellyfin.Api.Controllers
             }
 
             await using var memoryStream = new MemoryStream();
-            await new ProgressiveFileCopier(_streamHelper, path).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
+                .WriteToAsync(memoryStream, CancellationToken.None)
+                .ConfigureAwait(false);
             return File(memoryStream, MimeTypes.GetMimeType(path));
         }
 
@@ -1214,7 +1215,9 @@ namespace Jellyfin.Api.Controllers
             }
 
             await using var memoryStream = new MemoryStream();
-            await new ProgressiveFileCopier(_streamHelper, liveStreamInfo).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+            await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
+                .WriteToAsync(memoryStream, CancellationToken.None)
+                .ConfigureAwait(false);
             return File(memoryStream, MimeTypes.GetMimeType("file." + container));
         }
 
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 5050c3d4fe..0ce62186b0 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IMediaEncoder _mediaEncoder;
-        private readonly IStreamHelper _streamHelper;
         private readonly IFileSystem _fileSystem;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly IConfiguration _configuration;
@@ -67,7 +66,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
@@ -83,7 +81,6 @@ namespace Jellyfin.Api.Controllers
             IMediaSourceManager mediaSourceManager,
             IServerConfigurationManager serverConfigurationManager,
             IMediaEncoder mediaEncoder,
-            IStreamHelper streamHelper,
             IFileSystem fileSystem,
             ISubtitleEncoder subtitleEncoder,
             IConfiguration configuration,
@@ -99,7 +96,6 @@ namespace Jellyfin.Api.Controllers
             _mediaSourceManager = mediaSourceManager;
             _serverConfigurationManager = serverConfigurationManager;
             _mediaEncoder = mediaEncoder;
-            _streamHelper = streamHelper;
             _fileSystem = fileSystem;
             _subtitleEncoder = subtitleEncoder;
             _configuration = configuration;
@@ -376,7 +372,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Dictionary<string, string> streamOptions)
         {
             var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-            using var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationTokenSource = new CancellationTokenSource();
             var streamingRequest = new StreamingRequestDto
             {
                 Id = itemId,
@@ -453,8 +449,11 @@ namespace Jellyfin.Api.Controllers
             {
                 StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
 
-                // TODO AllowEndOfFile = false
-                await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    }.WriteToAsync(Response.Body, CancellationToken.None)
+                    .ConfigureAwait(false);
 
                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
                 return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@@ -489,8 +488,11 @@ namespace Jellyfin.Api.Controllers
 
                 if (state.MediaSource.IsInfiniteStream)
                 {
-                    // TODO AllowEndOfFile = false
-                    await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
+                    await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        }.WriteToAsync(Response.Body, CancellationToken.None)
+                        .ConfigureAwait(false);
 
                     return File(Response.Body, contentType);
                 }
@@ -509,7 +511,6 @@ namespace Jellyfin.Api.Controllers
             return await FileStreamResponseHelpers.GetTranscodedFile(
                 state,
                 isHeadRequest,
-                _streamHelper,
                 this,
                 _transcodingJobHelper,
                 ffmpegCommandLineArguments,
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 636f47f5f1..96e90d38fc 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -3,9 +3,9 @@ using System.IO;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Net.Http.Headers;
@@ -80,7 +80,6 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// <param name="state">The current <see cref="StreamState"/>.</param>
         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
@@ -91,7 +90,6 @@ namespace Jellyfin.Api.Helpers
         public static async Task<ActionResult> GetTranscodedFile(
             StreamState state,
             bool isHeadRequest,
-            IStreamHelper streamHelper,
             ControllerBase controller,
             TranscodingJobHelper transcodingJobHelper,
             string ffmpegCommandLineArguments,
@@ -116,18 +114,20 @@ namespace Jellyfin.Api.Helpers
             await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
             try
             {
+                TranscodingJobDto? job;
                 if (!File.Exists(outputPath))
                 {
-                    await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
+                    job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
                 }
                 else
                 {
-                    transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+                    job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
                     state.Dispose();
                 }
 
-                await using var memoryStream = new MemoryStream();
-                await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                var memoryStream = new MemoryStream();
+                await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
+                memoryStream.Position = 0;
                 return controller.File(memoryStream, contentType);
             }
             finally
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index e8e6966f45..acaccc77ae 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -2,6 +2,7 @@ using System;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models.PlaybackDtos;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
 
@@ -12,34 +13,53 @@ namespace Jellyfin.Api.Helpers
     /// </summary>
     public class ProgressiveFileCopier
     {
+        private readonly TranscodingJobDto? _job;
         private readonly string? _path;
+        private readonly CancellationToken _cancellationToken;
         private readonly IDirectStreamProvider? _directStreamProvider;
-        private readonly IStreamHelper _streamHelper;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private long _bytesWritten;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
-        /// <param name="path">Filepath to stream from.</param>
-        public ProgressiveFileCopier(IStreamHelper streamHelper, string path)
+        /// <param name="path">The path to copy from.</param>
+        /// <param name="job">The transcoding job.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
         {
             _path = path;
-            _streamHelper = streamHelper;
-            _directStreamProvider = null;
+            _job = job;
+            _cancellationToken = cancellationToken;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
         /// </summary>
-        /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
         /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
-        public ProgressiveFileCopier(IStreamHelper streamHelper, IDirectStreamProvider directStreamProvider)
+        /// <param name="job">The transcoding job.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
         {
             _directStreamProvider = directStreamProvider;
-            _streamHelper = streamHelper;
-            _path = null;
+            _job = job;
+            _cancellationToken = cancellationToken;
+            _transcodingJobHelper = transcodingJobHelper;
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether allow read end of file.
+        /// </summary>
+        public bool AllowEndOfFile { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets copy start position.
+        /// </summary>
+        public long StartPosition { get; set; }
+
         /// <summary>
         /// Write source stream to output.
         /// </summary>
@@ -48,37 +68,123 @@ namespace Jellyfin.Api.Helpers
         /// <returns>A <see cref="Task"/>.</returns>
         public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
         {
-            if (_directStreamProvider != null)
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+
+            try
             {
-                await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
-                return;
-            }
+                if (_directStreamProvider != null)
+                {
+                    await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
+                    return;
+                }
+
+                var fileOptions = FileOptions.SequentialScan;
+                var allowAsyncFileRead = false;
+
+                // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+                if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+                {
+                    fileOptions |= FileOptions.Asynchronous;
+                    allowAsyncFileRead = true;
+                }
+
+                await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+
+                var eofCount = 0;
+                const int emptyReadLimit = 20;
+                if (StartPosition > 0)
+                {
+                    inputStream.Position = StartPosition;
+                }
+
+                while (eofCount < emptyReadLimit || !AllowEndOfFile)
+                {
+                    int bytesRead;
+                    if (allowAsyncFileRead)
+                    {
+                        bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                    }
 
-            var fileOptions = FileOptions.SequentialScan;
+                    if (bytesRead == 0)
+                    {
+                        if (_job == null || _job.HasExited)
+                        {
+                            eofCount++;
+                        }
 
-            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        eofCount = 0;
+                    }
+                }
+            }
+            finally
             {
-                fileOptions |= FileOptions.Asynchronous;
+                if (_job != null)
+                {
+                    _transcodingJobHelper.OnTranscodeEndRequest(_job);
+                }
             }
+        }
 
-            await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions);
-            const int emptyReadLimit = 100;
-            var eofCount = 0;
-            while (eofCount < emptyReadLimit)
+        private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
+        {
+            var array = new byte[IODefaults.CopyToBufferSize];
+            int bytesRead;
+            int totalBytesRead = 0;
+
+            while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
             {
-                var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
+                var bytesToWrite = bytesRead;
 
-                if (bytesRead == 0)
+                if (bytesToWrite > 0)
                 {
-                    eofCount++;
-                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+                    _bytesWritten += bytesRead;
+                    totalBytesRead += bytesRead;
+
+                    if (_job != null)
+                    {
+                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                    }
                 }
-                else
+            }
+
+            return totalBytesRead;
+        }
+
+        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
+        {
+            var array = new byte[IODefaults.CopyToBufferSize];
+            int bytesRead;
+            int totalBytesRead = 0;
+
+            while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
+            {
+                var bytesToWrite = bytesRead;
+
+                if (bytesToWrite > 0)
                 {
-                    eofCount = 0;
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+
+                    _bytesWritten += bytesRead;
+                    totalBytesRead += bytesRead;
+
+                    if (_job != null)
+                    {
+                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
+                    }
                 }
             }
+
+            return totalBytesRead;
         }
     }
 }
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c84135085f..fc38eacafd 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -680,6 +680,20 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        /// <summary>
+        /// Called when [transcode end].
+        /// </summary>
+        /// <param name="job">The transcode job.</param>
+        public void OnTranscodeEndRequest(TranscodingJobDto job)
+        {
+            job.ActiveRequestCount--;
+            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
+            if (job.ActiveRequestCount <= 0)
+            {
+                PingTimer(job, false);
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// The progressive

From 592d2480ca9c424c7b8b8f4be2bdfa81b4476f0c Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 19:22:34 -0400
Subject: [PATCH 352/463] Make adjustments to display preference entities.

---
 Jellyfin.Data/Entities/DisplayPreferences.cs  |  65 ++--------
 .../Entities/LibraryDisplayPreferences.cs     | 120 ++++++++++++++++++
 Jellyfin.Data/Entities/User.cs                |  12 +-
 3 files changed, 143 insertions(+), 54 deletions(-)
 create mode 100644 Jellyfin.Data/Entities/LibraryDisplayPreferences.cs

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index bcb872db37..44b70d970c 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -14,14 +14,18 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
         /// </summary>
-        /// <param name="client">The client string.</param>
         /// <param name="userId">The user's id.</param>
-        public DisplayPreferences(string client, Guid userId)
+        /// <param name="client">The client string.</param>
+        public DisplayPreferences(Guid userId, string client)
         {
-            RememberIndexing = false;
-            ShowBackdrop = true;
-            Client = client;
             UserId = userId;
+            Client = client;
+            ShowSidebar = false;
+            ShowBackdrop = true;
+            SkipForwardLength = 30000;
+            SkipBackwardLength = 10000;
+            ScrollDirection = ScrollDirection.Horizontal;
+            ChromecastVersion = ChromecastVersion.Stable;
 
             HomeSections = new HashSet<HomeSection>();
         }
@@ -50,50 +54,17 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         public Guid UserId { get; set; }
 
-        /// <summary>
-        /// Gets or sets the id of the associated item.
-        /// </summary>
-        /// <remarks>
-        /// This is currently unused. In the future, this will allow us to have users set
-        /// display preferences per item.
-        /// </remarks>
-        public Guid? ItemId { get; set; }
-
         /// <summary>
         /// Gets or sets the client string.
         /// </summary>
         /// <remarks>
-        /// Required. Max Length = 64.
+        /// Required. Max Length = 32.
         /// </remarks>
         [Required]
-        [MaxLength(64)]
-        [StringLength(64)]
+        [MaxLength(32)]
+        [StringLength(32)]
         public string Client { get; set; }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether the indexing should be remembered.
-        /// </summary>
-        /// <remarks>
-        /// Required.
-        /// </remarks>
-        public bool RememberIndexing { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the sorting type should be remembered.
-        /// </summary>
-        /// <remarks>
-        /// Required.
-        /// </remarks>
-        public bool RememberSorting { get; set; }
-
-        /// <summary>
-        /// Gets or sets the sort order.
-        /// </summary>
-        /// <remarks>
-        /// Required.
-        /// </remarks>
-        public SortOrder SortOrder { get; set; }
-
         /// <summary>
         /// Gets or sets a value indicating whether to show the sidebar.
         /// </summary>
@@ -110,18 +81,6 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         public bool ShowBackdrop { get; set; }
 
-        /// <summary>
-        /// Gets or sets what the view should be sorted by.
-        /// </summary>
-        [MaxLength(64)]
-        [StringLength(64)]
-        public string SortBy { get; set; }
-
-        /// <summary>
-        /// Gets or sets the view type.
-        /// </summary>
-        public ViewType? ViewType { get; set; }
-
         /// <summary>
         /// Gets or sets the scroll direction.
         /// </summary>
diff --git a/Jellyfin.Data/Entities/LibraryDisplayPreferences.cs b/Jellyfin.Data/Entities/LibraryDisplayPreferences.cs
new file mode 100644
index 0000000000..87be1c6f7e
--- /dev/null
+++ b/Jellyfin.Data/Entities/LibraryDisplayPreferences.cs
@@ -0,0 +1,120 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Data.Entities
+{
+    public class LibraryDisplayPreferences
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LibraryDisplayPreferences"/> class.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="client">The client.</param>
+        public LibraryDisplayPreferences(Guid userId, Guid itemId, string client)
+        {
+            UserId = userId;
+            ItemId = itemId;
+            Client = client;
+
+            SortBy = "SortName";
+            ViewType = ViewType.Poster;
+            SortOrder = SortOrder.Ascending;
+            RememberSorting = false;
+            RememberIndexing = false;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LibraryDisplayPreferences"/> class.
+        /// </summary>
+        protected LibraryDisplayPreferences()
+        {
+        }
+
+        /// <summary>
+        /// Gets or sets the Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Gets or sets the user Id.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id of the associated item.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public Guid ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the client string.
+        /// </summary>
+        /// <remarks>
+        /// Required. Max Length = 32.
+        /// </remarks>
+        [Required]
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string Client { get; set; }
+
+        /// <summary>
+        /// Gets or sets the view type.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public ViewType ViewType { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the indexing should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool RememberIndexing { get; set; }
+
+        /// <summary>
+        /// Gets or sets what the view should be indexed by.
+        /// </summary>
+        public IndexingKind? IndexBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the sorting type should be remembered.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public bool RememberSorting { get; set; }
+
+        /// <summary>
+        /// Gets or sets what the view should be sorted by.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        [MaxLength(64)]
+        [StringLength(64)]
+        public string SortBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sort order.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        public SortOrder SortOrder { get; set; }
+    }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index d93144e3a5..dc4bd9979c 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
             PasswordResetProviderId = passwordResetProviderId;
 
             AccessSchedules = new HashSet<AccessSchedule>();
+            LibraryDisplayPreferences = new HashSet<LibraryDisplayPreferences>();
             // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
             Preferences = new HashSet<Preference>();
@@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
         // [ForeignKey("UserId")]
         public virtual ImageInfo ProfileImage { get; set; }
 
+        /// <summary>
+        /// Gets or sets the user's display preferences.
+        /// </summary>
+        /// <remarks>
+        /// Required.
+        /// </remarks>
+        [Required]
+        public virtual DisplayPreferences DisplayPreferences { get; set; }
+
         [Required]
         public SyncPlayAccess SyncPlayAccess { get; set; }
 
@@ -352,7 +362,7 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Gets or sets the list of item display preferences.
         /// </summary>
-        public virtual ICollection<DisplayPreferences> DisplayPreferences { get; protected set; }
+        public virtual ICollection<LibraryDisplayPreferences> LibraryDisplayPreferences { get; protected set; }
 
         /*
         /// <summary>

From 68a185fd02844698ac5ecd5618d590ae254d95cf Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 20:40:21 -0400
Subject: [PATCH 353/463] Serialize/deserialize new entities properly.

---
 ...eferences.cs => ItemDisplayPreferences.cs} | 10 +--
 Jellyfin.Data/Entities/User.cs                |  4 +-
 Jellyfin.Server.Implementations/JellyfinDb.cs |  2 +
 .../Users/DisplayPreferencesManager.cs        | 39 ++++++++++-
 .../Routines/MigrateDisplayPreferencesDb.cs   | 33 +++++++--
 MediaBrowser.Api/DisplayPreferencesService.cs | 70 ++++++++++++-------
 .../IDisplayPreferencesManager.cs             | 24 +++++++
 7 files changed, 139 insertions(+), 43 deletions(-)
 rename Jellyfin.Data/Entities/{LibraryDisplayPreferences.cs => ItemDisplayPreferences.cs} (89%)

diff --git a/Jellyfin.Data/Entities/LibraryDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
similarity index 89%
rename from Jellyfin.Data/Entities/LibraryDisplayPreferences.cs
rename to Jellyfin.Data/Entities/ItemDisplayPreferences.cs
index 87be1c6f7e..95c08e6c6c 100644
--- a/Jellyfin.Data/Entities/LibraryDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -5,15 +5,15 @@ using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data.Entities
 {
-    public class LibraryDisplayPreferences
+    public class ItemDisplayPreferences
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryDisplayPreferences"/> class.
+        /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
         /// </summary>
         /// <param name="userId">The user id.</param>
         /// <param name="itemId">The item id.</param>
         /// <param name="client">The client.</param>
-        public LibraryDisplayPreferences(Guid userId, Guid itemId, string client)
+        public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
         {
             UserId = userId;
             ItemId = itemId;
@@ -27,9 +27,9 @@ namespace Jellyfin.Data.Entities
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryDisplayPreferences"/> class.
+        /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
         /// </summary>
-        protected LibraryDisplayPreferences()
+        protected ItemDisplayPreferences()
         {
         }
 
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index dc4bd9979c..50810561f5 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -48,7 +48,7 @@ namespace Jellyfin.Data.Entities
             PasswordResetProviderId = passwordResetProviderId;
 
             AccessSchedules = new HashSet<AccessSchedule>();
-            LibraryDisplayPreferences = new HashSet<LibraryDisplayPreferences>();
+            ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
             // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
             Preferences = new HashSet<Preference>();
@@ -362,7 +362,7 @@ namespace Jellyfin.Data.Entities
         /// <summary>
         /// Gets or sets the list of item display preferences.
         /// </summary>
-        public virtual ICollection<LibraryDisplayPreferences> LibraryDisplayPreferences { get; protected set; }
+        public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
 
         /*
         /// <summary>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 4ad6840639..7d864ebc69 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -32,6 +32,8 @@ namespace Jellyfin.Server.Implementations
 
         public virtual DbSet<ImageInfo> ImageInfos { get; set; }
 
+        public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
+
         public virtual DbSet<Permission> Permissions { get; set; }
 
         public virtual DbSet<Preference> Preferences { get; set; }
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index b7c65fc2cb..7c5c5a3ec5 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -1,6 +1,7 @@
 #pragma warning disable CA1307
 
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller;
@@ -31,17 +32,43 @@ namespace Jellyfin.Server.Implementations.Users
             var prefs = dbContext.DisplayPreferences
                 .Include(pref => pref.HomeSections)
                 .FirstOrDefault(pref =>
-                    pref.UserId == userId && pref.ItemId == null && string.Equals(pref.Client, client));
+                    pref.UserId == userId && string.Equals(pref.Client, client));
 
             if (prefs == null)
             {
-                prefs = new DisplayPreferences(client, userId);
+                prefs = new DisplayPreferences(userId, client);
                 dbContext.DisplayPreferences.Add(prefs);
             }
 
             return prefs;
         }
 
+        /// <inheritdoc />
+        public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            var prefs = dbContext.ItemDisplayPreferences
+                .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
+
+            if (prefs == null)
+            {
+                prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
+                dbContext.ItemDisplayPreferences.Add(prefs);
+            }
+
+            return prefs;
+        }
+
+        /// <inheritdoc />
+        public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+
+            return dbContext.ItemDisplayPreferences
+                .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
+                .ToList();
+        }
+
         /// <inheritdoc />
         public void SaveChanges(DisplayPreferences preferences)
         {
@@ -49,5 +76,13 @@ namespace Jellyfin.Server.Implementations.Users
             dbContext.Update(preferences);
             dbContext.SaveChanges();
         }
+
+        /// <inheritdoc />
+        public void SaveChanges(ItemDisplayPreferences preferences)
+        {
+            using var dbContext = _dbProvider.CreateContext();
+            dbContext.Update(preferences);
+            dbContext.SaveChanges();
+        }
     }
 }
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index d8b081bb29..6a78bff4fa 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Entities;
@@ -78,16 +79,11 @@ namespace Jellyfin.Server.Migrations.Routines
                             : ChromecastVersion.Stable
                         : ChromecastVersion.Stable;
 
-                    var displayPreferences = new DisplayPreferences(result[2].ToString(), new Guid(result[1].ToBlob()))
+                    var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
                     {
-                        ViewType = Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType) ? viewType : (ViewType?)null,
                         IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
                         ShowBackdrop = dto.ShowBackdrop,
                         ShowSidebar = dto.ShowSidebar,
-                        SortBy = dto.SortBy,
-                        SortOrder = dto.SortOrder,
-                        RememberIndexing = dto.RememberIndexing,
-                        RememberSorting = dto.RememberSorting,
                         ScrollDirection = dto.ScrollDirection,
                         ChromecastVersion = chromecastVersion,
                         SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
@@ -95,7 +91,7 @@ namespace Jellyfin.Server.Migrations.Routines
                             : 30000,
                         SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
                             ? int.Parse(length, CultureInfo.InvariantCulture)
-                            : 30000,
+                            : 10000,
                         EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
                             ? bool.Parse(enabled)
                             : true
@@ -112,6 +108,29 @@ namespace Jellyfin.Server.Migrations.Routines
                         });
                     }
 
+                    foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
+                    {
+                        if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))
+                        {
+                            continue;
+                        }
+
+                        var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
+                        {
+                            SortBy = dto.SortBy,
+                            SortOrder = dto.SortOrder,
+                            RememberIndexing = dto.RememberIndexing,
+                            RememberSorting = dto.RememberSorting,
+                        };
+
+                        if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType))
+                        {
+                            libraryDisplayPreferences.ViewType = viewType;
+                        }
+
+                        dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
+                    }
+
                     dbContext.Add(displayPreferences);
                 }
 
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 0f3427e541..416d63100f 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -76,37 +76,38 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public object Get(GetDisplayPreferences request)
         {
-            var result = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
-
-            if (result == null)
-            {
-                return null;
-            }
+            var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
+            var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
 
             var dto = new DisplayPreferencesDto
             {
-                Client = result.Client,
-                Id = result.UserId.ToString(),
-                ViewType = result.ViewType?.ToString(),
-                SortBy = result.SortBy,
-                SortOrder = result.SortOrder,
-                IndexBy = result.IndexBy?.ToString(),
-                RememberIndexing = result.RememberIndexing,
-                RememberSorting = result.RememberSorting,
-                ScrollDirection = result.ScrollDirection,
-                ShowBackdrop = result.ShowBackdrop,
-                ShowSidebar = result.ShowSidebar
+                Client = displayPreferences.Client,
+                Id = displayPreferences.UserId.ToString(),
+                ViewType = itemPreferences.ViewType.ToString(),
+                SortBy = itemPreferences.SortBy,
+                SortOrder = itemPreferences.SortOrder,
+                IndexBy = displayPreferences.IndexBy?.ToString(),
+                RememberIndexing = itemPreferences.RememberIndexing,
+                RememberSorting = itemPreferences.RememberSorting,
+                ScrollDirection = displayPreferences.ScrollDirection,
+                ShowBackdrop = displayPreferences.ShowBackdrop,
+                ShowSidebar = displayPreferences.ShowSidebar
             };
 
-            foreach (var homeSection in result.HomeSections)
+            foreach (var homeSection in displayPreferences.HomeSections)
             {
                 dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
             }
 
-            dto.CustomPrefs["chromecastVersion"] = result.ChromecastVersion.ToString().ToLowerInvariant();
-            dto.CustomPrefs["skipForwardLength"] = result.SkipForwardLength.ToString();
-            dto.CustomPrefs["skipBackLength"] = result.SkipBackwardLength.ToString();
-            dto.CustomPrefs["enableNextVideoInfoOverlay"] = result.EnableNextVideoInfoOverlay.ToString();
+            foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
+            {
+                dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
+            }
+
+            dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
+            dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString();
+            dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString();
+            dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString();
 
             return ToOptimizedResult(dto);
         }
@@ -130,14 +131,10 @@ namespace MediaBrowser.Api
 
             var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
 
-            prefs.ViewType = Enum.TryParse<ViewType>(request.ViewType, true, out var viewType) ? viewType : (ViewType?)null;
             prefs.IndexBy = Enum.TryParse<IndexingKind>(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
             prefs.ShowBackdrop = request.ShowBackdrop;
             prefs.ShowSidebar = request.ShowSidebar;
-            prefs.SortBy = request.SortBy;
-            prefs.SortOrder = request.SortOrder;
-            prefs.RememberIndexing = request.RememberIndexing;
-            prefs.RememberSorting = request.RememberSorting;
+
             prefs.ScrollDirection = request.ScrollDirection;
             prefs.ChromecastVersion = request.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
                 ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
@@ -164,7 +161,26 @@ namespace MediaBrowser.Api
                 });
             }
 
+            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("landing-")))
+            {
+                var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Parse(key.Substring("landing-".Length)), prefs.Client);
+                itemPreferences.ViewType = Enum.Parse<ViewType>(request.ViewType);
+                _displayPreferencesManager.SaveChanges(itemPreferences);
+            }
+
+            var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Empty, prefs.Client);
+            itemPrefs.SortBy = request.SortBy;
+            itemPrefs.SortOrder = request.SortOrder;
+            itemPrefs.RememberIndexing = request.RememberIndexing;
+            itemPrefs.RememberSorting = request.RememberSorting;
+
+            if (Enum.TryParse<ViewType>(request.ViewType, true, out var viewType))
+            {
+                itemPrefs.ViewType = viewType;
+            }
+
             _displayPreferencesManager.SaveChanges(prefs);
+            _displayPreferencesManager.SaveChanges(itemPrefs);
         }
     }
 }
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index e27b0ec7c3..b6bfed3e59 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Jellyfin.Data.Entities;
 
 namespace MediaBrowser.Controller
@@ -16,10 +17,33 @@ namespace MediaBrowser.Controller
         /// <returns>The associated display preferences.</returns>
         DisplayPreferences GetDisplayPreferences(Guid userId, string client);
 
+        /// <summary>
+        /// Gets the default item display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>The item display preferences.</returns>
+        ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client);
+
+        /// <summary>
+        /// Gets all of the item display preferences for the user and client.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="client">The client string.</param>
+        /// <returns>A list of item display preferences.</returns>
+        IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
+
         /// <summary>
         /// Saves changes to the provided display preferences.
         /// </summary>
         /// <param name="preferences">The display preferences to save.</param>
         void SaveChanges(DisplayPreferences preferences);
+
+        /// <summary>
+        /// Saves changes to the provided item display preferences.
+        /// </summary>
+        /// <param name="preferences">The item display preferences to save.</param>
+        void SaveChanges(ItemDisplayPreferences preferences);
     }
 }

From 7d6c1befe51ec20c4727426ae7bff8bb316e3495 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 20:44:46 -0400
Subject: [PATCH 354/463] Add Dashboard theme.

---
 Jellyfin.Data/Entities/DisplayPreferences.cs  | 8 ++++++++
 MediaBrowser.Api/DisplayPreferencesService.cs | 1 +
 2 files changed, 9 insertions(+)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 44b70d970c..d2cf991958 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -26,6 +26,7 @@ namespace Jellyfin.Data.Entities
             SkipBackwardLength = 10000;
             ScrollDirection = ScrollDirection.Horizontal;
             ChromecastVersion = ChromecastVersion.Stable;
+            DashboardTheme = string.Empty;
 
             HomeSections = new HashSet<HomeSection>();
         }
@@ -126,6 +127,13 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         public bool EnableNextVideoInfoOverlay { get; set; }
 
+        /// <summary>
+        /// Gets or sets the dashboard theme.
+        /// </summary>
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string DashboardTheme { get; set; }
+
         /// <summary>
         /// Gets or sets the home sections.
         /// </summary>
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 416d63100f..2cc7db6242 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -144,6 +144,7 @@ namespace MediaBrowser.Api
                 : true;
             prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
             prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
+            prefs.DashboardTheme = request.CustomPrefs.TryGetValue("dashboardTheme", out var theme) ? theme : string.Empty;
             prefs.HomeSections.Clear();
 
             foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))

From 754837f16fef1c56cc8ccb30b75a0e316a72906a Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 20:50:58 -0400
Subject: [PATCH 355/463] Add tv home.

---
 Jellyfin.Data/Entities/DisplayPreferences.cs              | 8 ++++++++
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs    | 4 +++-
 MediaBrowser.Api/DisplayPreferencesService.cs             | 2 ++
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index d2cf991958..cda83f6bb7 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -27,6 +27,7 @@ namespace Jellyfin.Data.Entities
             ScrollDirection = ScrollDirection.Horizontal;
             ChromecastVersion = ChromecastVersion.Stable;
             DashboardTheme = string.Empty;
+            TvHome = string.Empty;
 
             HomeSections = new HashSet<HomeSection>();
         }
@@ -134,6 +135,13 @@ namespace Jellyfin.Data.Entities
         [StringLength(32)]
         public string DashboardTheme { get; set; }
 
+        /// <summary>
+        /// Gets or sets the tv home screen.
+        /// </summary>
+        [MaxLength(32)]
+        [StringLength(32)]
+        public string TvHome { get; set; }
+
         /// <summary>
         /// Gets or sets the home sections.
         /// </summary>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 6a78bff4fa..cef2c74351 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -94,7 +94,9 @@ namespace Jellyfin.Server.Migrations.Routines
                             : 10000,
                         EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
                             ? bool.Parse(enabled)
-                            : true
+                            : true,
+                        DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
+                        TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
                     };
 
                     for (int i = 0; i < 7; i++)
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 2cc7db6242..4951dcc22c 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -108,6 +108,7 @@ namespace MediaBrowser.Api
             dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString();
             dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString();
             dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString();
+            dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
 
             return ToOptimizedResult(dto);
         }
@@ -145,6 +146,7 @@ namespace MediaBrowser.Api
             prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
             prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
             prefs.DashboardTheme = request.CustomPrefs.TryGetValue("dashboardTheme", out var theme) ? theme : string.Empty;
+            prefs.TvHome = request.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty;
             prefs.HomeSections.Clear();
 
             foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))

From 4b8ab1a8030e13e4e0905f5f0b0dfcbdb97e7966 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 21:01:08 -0400
Subject: [PATCH 356/463] Set default value of SortBy during migrations.

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index cef2c74351..e76d45da7e 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -119,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
 
                         var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
                         {
-                            SortBy = dto.SortBy,
+                            SortBy = dto.SortBy ?? "SortName",
                             SortOrder = dto.SortOrder,
                             RememberIndexing = dto.RememberIndexing,
                             RememberSorting = dto.RememberSorting,

From c3a36485b68c7eeaffd3b60af14a8ef051c25f96 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Mon, 27 Jul 2020 23:41:16 -0400
Subject: [PATCH 357/463] Recreate display preferences migration.

---
 ...8005145_AddDisplayPreferences.Designer.cs} | 89 ++++++++++++++-----
 ...> 20200728005145_AddDisplayPreferences.cs} | 54 +++++++++--
 .../Migrations/JellyfinDbModelSnapshot.cs     | 87 +++++++++++++-----
 3 files changed, 176 insertions(+), 54 deletions(-)
 rename Jellyfin.Server.Implementations/Migrations/{20200717233541_AddDisplayPreferences.Designer.cs => 20200728005145_AddDisplayPreferences.Designer.cs} (88%)
 rename Jellyfin.Server.Implementations/Migrations/{20200717233541_AddDisplayPreferences.cs => 20200728005145_AddDisplayPreferences.cs} (68%)

diff --git a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
similarity index 88%
rename from Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
rename to Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
index 392e26daef..d44707d069 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
@@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 namespace Jellyfin.Server.Implementations.Migrations
 {
     [DbContext(typeof(JellyfinDb))]
-    [Migration("20200717233541_AddDisplayPreferences")]
+    [Migration("20200728005145_AddDisplayPreferences")]
     partial class AddDisplayPreferences
     {
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.5");
+                .HasAnnotation("ProductVersion", "3.1.6");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -104,7 +104,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<string>("Client")
                         .IsRequired()
                         .HasColumnType("TEXT")
-                        .HasMaxLength(64);
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
 
                     b.Property<bool>("EnableNextVideoInfoOverlay")
                         .HasColumnType("INTEGER");
@@ -112,15 +116,6 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int?>("IndexBy")
                         .HasColumnType("INTEGER");
 
-                    b.Property<Guid?>("ItemId")
-                        .HasColumnType("TEXT");
-
-                    b.Property<bool>("RememberIndexing")
-                        .HasColumnType("INTEGER");
-
-                    b.Property<bool>("RememberSorting")
-                        .HasColumnType("INTEGER");
-
                     b.Property<int>("ScrollDirection")
                         .HasColumnType("INTEGER");
 
@@ -136,22 +131,17 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("SkipForwardLength")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("SortBy")
+                    b.Property<string>("TvHome")
                         .HasColumnType("TEXT")
-                        .HasMaxLength(64);
-
-                    b.Property<int>("SortOrder")
-                        .HasColumnType("INTEGER");
+                        .HasMaxLength(32);
 
                     b.Property<Guid>("UserId")
                         .HasColumnType("TEXT");
 
-                    b.Property<int?>("ViewType")
-                        .HasColumnType("INTEGER");
-
                     b.HasKey("Id");
 
-                    b.HasIndex("UserId");
+                    b.HasIndex("UserId")
+                        .IsUnique();
 
                     b.ToTable("DisplayPreferences");
                 });
@@ -203,6 +193,50 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("ImageInfos");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.Property<int>("Id")
@@ -375,8 +409,8 @@ namespace Jellyfin.Server.Implementations.Migrations
             modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
-                        .WithMany("DisplayPreferences")
-                        .HasForeignKey("UserId")
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -397,6 +431,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
diff --git a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
similarity index 68%
rename from Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
rename to Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
index a5c344fac0..3009f0b61d 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200717233541_AddDisplayPreferences.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
@@ -18,21 +18,17 @@ namespace Jellyfin.Server.Implementations.Migrations
                     Id = table.Column<int>(nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
                     UserId = table.Column<Guid>(nullable: false),
-                    ItemId = table.Column<Guid>(nullable: true),
-                    Client = table.Column<string>(maxLength: 64, nullable: false),
-                    RememberIndexing = table.Column<bool>(nullable: false),
-                    RememberSorting = table.Column<bool>(nullable: false),
-                    SortOrder = table.Column<int>(nullable: false),
+                    Client = table.Column<string>(maxLength: 32, nullable: false),
                     ShowSidebar = table.Column<bool>(nullable: false),
                     ShowBackdrop = table.Column<bool>(nullable: false),
-                    SortBy = table.Column<string>(maxLength: 64, nullable: true),
-                    ViewType = table.Column<int>(nullable: true),
                     ScrollDirection = table.Column<int>(nullable: false),
                     IndexBy = table.Column<int>(nullable: true),
                     SkipForwardLength = table.Column<int>(nullable: false),
                     SkipBackwardLength = table.Column<int>(nullable: false),
                     ChromecastVersion = table.Column<int>(nullable: false),
-                    EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false)
+                    EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false),
+                    DashboardTheme = table.Column<string>(maxLength: 32, nullable: true),
+                    TvHome = table.Column<string>(maxLength: 32, nullable: true)
                 },
                 constraints: table =>
                 {
@@ -46,6 +42,35 @@ namespace Jellyfin.Server.Implementations.Migrations
                         onDelete: ReferentialAction.Cascade);
                 });
 
+            migrationBuilder.CreateTable(
+                name: "ItemDisplayPreferences",
+                schema: "jellyfin",
+                columns: table => new
+                {
+                    Id = table.Column<int>(nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    UserId = table.Column<Guid>(nullable: false),
+                    ItemId = table.Column<Guid>(nullable: false),
+                    Client = table.Column<string>(maxLength: 32, nullable: false),
+                    ViewType = table.Column<int>(nullable: false),
+                    RememberIndexing = table.Column<bool>(nullable: false),
+                    IndexBy = table.Column<int>(nullable: true),
+                    RememberSorting = table.Column<bool>(nullable: false),
+                    SortBy = table.Column<string>(maxLength: 64, nullable: false),
+                    SortOrder = table.Column<int>(nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_ItemDisplayPreferences_Users_UserId",
+                        column: x => x.UserId,
+                        principalSchema: "jellyfin",
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
             migrationBuilder.CreateTable(
                 name: "HomeSection",
                 schema: "jellyfin",
@@ -73,13 +98,20 @@ namespace Jellyfin.Server.Implementations.Migrations
                 name: "IX_DisplayPreferences_UserId",
                 schema: "jellyfin",
                 table: "DisplayPreferences",
-                column: "UserId");
+                column: "UserId",
+                unique: true);
 
             migrationBuilder.CreateIndex(
                 name: "IX_HomeSection_DisplayPreferencesId",
                 schema: "jellyfin",
                 table: "HomeSection",
                 column: "DisplayPreferencesId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemDisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "ItemDisplayPreferences",
+                column: "UserId");
         }
 
         protected override void Down(MigrationBuilder migrationBuilder)
@@ -88,6 +120,10 @@ namespace Jellyfin.Server.Implementations.Migrations
                 name: "HomeSection",
                 schema: "jellyfin");
 
+            migrationBuilder.DropTable(
+                name: "ItemDisplayPreferences",
+                schema: "jellyfin");
+
             migrationBuilder.DropTable(
                 name: "DisplayPreferences",
                 schema: "jellyfin");
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 76de592ac7..a6e6a23249 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.5");
+                .HasAnnotation("ProductVersion", "3.1.6");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -100,7 +100,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<string>("Client")
                         .IsRequired()
                         .HasColumnType("TEXT")
-                        .HasMaxLength(64);
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
 
                     b.Property<bool>("EnableNextVideoInfoOverlay")
                         .HasColumnType("INTEGER");
@@ -108,15 +112,6 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int?>("IndexBy")
                         .HasColumnType("INTEGER");
 
-                    b.Property<Guid?>("ItemId")
-                        .HasColumnType("TEXT");
-
-                    b.Property<bool>("RememberIndexing")
-                        .HasColumnType("INTEGER");
-
-                    b.Property<bool>("RememberSorting")
-                        .HasColumnType("INTEGER");
-
                     b.Property<int>("ScrollDirection")
                         .HasColumnType("INTEGER");
 
@@ -132,22 +127,17 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int>("SkipForwardLength")
                         .HasColumnType("INTEGER");
 
-                    b.Property<string>("SortBy")
+                    b.Property<string>("TvHome")
                         .HasColumnType("TEXT")
-                        .HasMaxLength(64);
-
-                    b.Property<int>("SortOrder")
-                        .HasColumnType("INTEGER");
+                        .HasMaxLength(32);
 
                     b.Property<Guid>("UserId")
                         .HasColumnType("TEXT");
 
-                    b.Property<int?>("ViewType")
-                        .HasColumnType("INTEGER");
-
                     b.HasKey("Id");
 
-                    b.HasIndex("UserId");
+                    b.HasIndex("UserId")
+                        .IsUnique();
 
                     b.ToTable("DisplayPreferences");
                 });
@@ -199,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("ImageInfos");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.Property<int>("Id")
@@ -371,8 +405,8 @@ namespace Jellyfin.Server.Implementations.Migrations
             modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)
-                        .WithMany("DisplayPreferences")
-                        .HasForeignKey("UserId")
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -393,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.User", null)

From 2139e9f8d11597a06839df7bb1b88787bf750305 Mon Sep 17 00:00:00 2001
From: Nyanmisaka <nst799610810@gmail.com>
Date: Tue, 28 Jul 2020 17:07:10 +0800
Subject: [PATCH 358/463] adjust priority in outputSizeParam cutter

---
 .../MediaEncoding/EncodingHelper.cs            | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 913e171f14..18d310a009 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1606,42 +1606,48 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
 
-                var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
+                // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
+                var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
                 if (index != -1)
                 {
                     outputSizeParam = outputSizeParam.Slice(index);
                 }
                 else
                 {
-                    index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
+                    // vpp_qsv
+                    index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
                     if (index != -1)
                     {
                         outputSizeParam = outputSizeParam.Slice(index);
                     }
                     else
                     {
-                        index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+                        // hwdownload,format=p010le (hardware decode + software encode for vaapi)
+                        index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
                         if (index != -1)
                         {
                             outputSizeParam = outputSizeParam.Slice(index);
                         }
                         else
                         {
-                            index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+                            // format=nv12|vaapi,hwupload,scale_vaapi
+                            index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
                             if (index != -1)
                             {
                                 outputSizeParam = outputSizeParam.Slice(index);
                             }
                             else
                             {
-                                index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+                                // yadif,scale=expr
+                                index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
                                 if (index != -1)
                                 {
                                     outputSizeParam = outputSizeParam.Slice(index);
                                 }
                                 else
                                 {
-                                    index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
+                                    // scale=expr
+                                    index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
                                     if (index != -1)
                                     {
                                         outputSizeParam = outputSizeParam.Slice(index);

From c094916df0e6202356602dc191516e3b0f87f429 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Tue, 28 Jul 2020 09:19:25 -0400
Subject: [PATCH 359/463] Migrate default library display preferences.

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index e76d45da7e..183b1aeabd 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -110,6 +110,16 @@ namespace Jellyfin.Server.Migrations.Routines
                         });
                     }
 
+                    var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
+                    {
+                        SortBy = dto.SortBy ?? "SortName",
+                        SortOrder = dto.SortOrder,
+                        RememberIndexing = dto.RememberIndexing,
+                        RememberSorting = dto.RememberSorting,
+                    };
+
+                    dbContext.Add(defaultLibraryPrefs);
+
                     foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
                     {
                         if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))

From 995c8fadb7ce7821eaddc7ae9245cadc2e9ddc05 Mon Sep 17 00:00:00 2001
From: Patrick Barron <18354464+barronpm@users.noreply.github.com>
Date: Tue, 28 Jul 2020 21:17:11 +0000
Subject: [PATCH 360/463] Update MediaBrowser.Api/DisplayPreferencesService.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 MediaBrowser.Api/DisplayPreferencesService.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
index 4951dcc22c..559b71efc7 100644
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ b/MediaBrowser.Api/DisplayPreferencesService.cs
@@ -52,7 +52,7 @@ namespace MediaBrowser.Api
     public class DisplayPreferencesService : BaseApiService
     {
         /// <summary>
-        /// The user manager.
+        /// The display preferences manager.
         /// </summary>
         private readonly IDisplayPreferencesManager _displayPreferencesManager;
 

From 03f15fc0eff3a8937e2c710781016eab9e6b9776 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Jul 2020 09:28:53 -0600
Subject: [PATCH 361/463] use proper os comparison

---
 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index acaccc77ae..d8b1828f5d 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -1,5 +1,7 @@
 using System;
+using System.Buffers;
 using System.IO;
+using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Models.PlaybackDtos;
@@ -82,7 +84,7 @@ namespace Jellyfin.Api.Helpers
                 var allowAsyncFileRead = false;
 
                 // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-                if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                 {
                     fileOptions |= FileOptions.Asynchronous;
                     allowAsyncFileRead = true;

From d0ce239e3e71d52fd405cd7981cecb5ee49983a5 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Jul 2020 12:12:54 -0600
Subject: [PATCH 362/463] Use ArrayPool and reduce duplicate code

---
 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 59 +++++++------------
 1 file changed, 21 insertions(+), 38 deletions(-)

diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index d8b1828f5d..71197d931f 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -93,23 +93,15 @@ namespace Jellyfin.Api.Helpers
                 await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
 
                 var eofCount = 0;
-                const int emptyReadLimit = 20;
+                const int EmptyReadLimit = 20;
                 if (StartPosition > 0)
                 {
                     inputStream.Position = StartPosition;
                 }
 
-                while (eofCount < emptyReadLimit || !AllowEndOfFile)
+                while (eofCount < EmptyReadLimit || !AllowEndOfFile)
                 {
-                    int bytesRead;
-                    if (allowAsyncFileRead)
-                    {
-                        bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-                    }
+                    var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
 
                     if (bytesRead == 0)
                     {
@@ -135,40 +127,22 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
-        private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
+        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
         {
-            var array = new byte[IODefaults.CopyToBufferSize];
+            var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
             int bytesRead;
             int totalBytesRead = 0;
 
-            while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
+            if (readAsync)
             {
-                var bytesToWrite = bytesRead;
-
-                if (bytesToWrite > 0)
-                {
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                    _bytesWritten += bytesRead;
-                    totalBytesRead += bytesRead;
-
-                    if (_job != null)
-                    {
-                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
-                    }
-                }
+                bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+            }
+            else
+            {
+                bytesRead = source.Read(array, 0, array.Length);
             }
 
-            return totalBytesRead;
-        }
-
-        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            var array = new byte[IODefaults.CopyToBufferSize];
-            int bytesRead;
-            int totalBytesRead = 0;
-
-            while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
+            while (bytesRead != 0)
             {
                 var bytesToWrite = bytesRead;
 
@@ -184,6 +158,15 @@ namespace Jellyfin.Api.Helpers
                         _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
                     }
                 }
+
+                if (readAsync)
+                {
+                    bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    bytesRead = source.Read(array, 0, array.Length);
+                }
             }
 
             return totalBytesRead;

From 5c4b342323bdf637738c3c87efcce625ae748f34 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Jul 2020 14:21:32 -0600
Subject: [PATCH 363/463] fix boolean

---
 Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index 71197d931f..432df97086 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Helpers
                 var allowAsyncFileRead = false;
 
                 // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                 {
                     fileOptions |= FileOptions.Asynchronous;
                     allowAsyncFileRead = true;

From df5d43132cd7a59bd912ba7bf6e67fdc1605acc8 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Jul 2020 16:42:01 -0600
Subject: [PATCH 364/463] Fix PlaybackInfo endpoint

---
 Jellyfin.Api/Controllers/MediaInfoController.cs   |  7 ++++---
 Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs | 15 +++++++++++++++
 2 files changed, 19 insertions(+), 3 deletions(-)
 create mode 100644 Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs

diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index da400f5106..c2c02c02ca 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -7,6 +7,7 @@ using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.VideoDtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? maxAudioChannels,
             [FromQuery] string? mediaSourceId,
             [FromQuery] string? liveStreamId,
-            [FromQuery] DeviceProfile? deviceProfile,
+            [FromBody] DeviceProfileDto? deviceProfile,
             [FromQuery] bool autoOpenLiveStream = false,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectStream = true,
@@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers
         {
             var authInfo = _authContext.GetAuthorizationInfo(Request);
 
-            var profile = deviceProfile;
+            var profile = deviceProfile?.DeviceProfile;
 
             _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
 
@@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers
                     var openStreamResult = await OpenMediaSource(new LiveStreamRequest
                     {
                         AudioStreamIndex = audioStreamIndex,
-                        DeviceProfile = deviceProfile,
+                        DeviceProfile = deviceProfile?.DeviceProfile,
                         EnableDirectPlay = enableDirectPlay,
                         EnableDirectStream = enableDirectStream,
                         ItemId = itemId,
diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs
new file mode 100644
index 0000000000..db55dc34b5
--- /dev/null
+++ b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Dlna;
+
+namespace Jellyfin.Api.Models.VideoDtos
+{
+    /// <summary>
+    /// Device profile dto.
+    /// </summary>
+    public class DeviceProfileDto
+    {
+        /// <summary>
+        /// Gets or sets device profile.
+        /// </summary>
+        public DeviceProfile? DeviceProfile { get; set; }
+    }
+}

From bb1fec9da1a1495eb1a259802b9fcd27f69b40fb Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Wed, 29 Jul 2020 16:43:53 -0600
Subject: [PATCH 365/463] remove extra using

---
 Jellyfin.Api/Controllers/AudioController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index e63868339f..1d81def725 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
         {
             bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
 
-            using var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationTokenSource = new CancellationTokenSource();
 
             StreamingRequestDto streamingRequest = new StreamingRequestDto
             {

From f543a17d1b8a4e1e2f420831de586e9ecc015166 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 30 Jul 2020 06:29:06 -0600
Subject: [PATCH 366/463] Apply review fixes

---
 Jellyfin.Api/Controllers/VideosController.cs      | 2 +-
 Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 0ce62186b0..fb02ec9617 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -373,7 +373,7 @@ namespace Jellyfin.Api.Controllers
         {
             var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
             var cancellationTokenSource = new CancellationTokenSource();
-            var streamingRequest = new StreamingRequestDto
+            var streamingRequest = new VideoRequestDto
             {
                 Id = itemId,
                 Container = container,
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 96e90d38fc..a463783e00 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -71,8 +71,7 @@ namespace Jellyfin.Api.Helpers
                 return controller.NoContent();
             }
 
-            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
-            return controller.File(stream, contentType);
+            return controller.PhysicalFile(path, contentType);
         }
 
         /// <summary>

From 8a016e31f74fe02ad10bf92dabc370eb96388306 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 31 Jul 2020 17:09:10 +0200
Subject: [PATCH 367/463] Move VideoHlsService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AudioController.cs   |   1 +
 .../Controllers/VideoHlsController.cs         | 502 ++++++++++++++++++
 Jellyfin.Api/Helpers/HlsHelpers.cs            |  95 ++++
 .../Playback/Hls/VideoHlsService.cs           | 167 ------
 4 files changed, 598 insertions(+), 167 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/VideoHlsController.cs
 create mode 100644 Jellyfin.Api/Helpers/HlsHelpers.cs

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 7405c26fb8..9a789989b6 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -146,6 +146,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Audio file returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
new file mode 100644
index 0000000000..bd85a684ac
--- /dev/null
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -0,0 +1,502 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The video hls controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class VideoHlsController : BaseJellyfinApiController
+    {
+        private const string DefaultEncoderPreset = "superfast";
+        private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
+        private readonly EncodingHelper _encodingHelper;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly ILogger _logger;
+        private readonly EncodingOptions _encodingOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="VideoHlsController"/> class.
+        /// </summary>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param>
+        public VideoHlsController(
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDlnaManager dlnaManager,
+            IUserManager userManger,
+            IAuthorizationContext authorizationContext,
+            ILibraryManager libraryManager,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            ILogger<VideoHlsController> logger)
+        {
+            _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
+            _dlnaManager = dlnaManager;
+            _authContext = authorizationContext;
+            _userManager = userManger;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _logger = logger;
+            _encodingOptions = serverConfigurationManager.GetEncodingOptions();
+        }
+
+        /// <summary>
+        /// Gets a hls live stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The audio container.</param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <param name="maxWidth">Optional. The max width.</param>
+        /// <param name="maxHeight">Optional. The max height.</param>
+        /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
+        /// <response code="200">Hls live stream retreaved.</response>
+        /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
+        [HttpGet("/Videos/{itemId}/live.m3u8")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetLiveHlsStream(
+            [FromRoute] Guid itemId,
+            [FromQuery] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] bool? enableSubtitlesInManifest)
+        {
+            VideoRequestDto streamingRequest = new VideoRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions,
+                MaxHeight = maxHeight,
+                MaxWidth = maxWidth,
+                EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+            };
+
+            var cancellationTokenSource = new CancellationTokenSource();
+            var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    TranscodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            TranscodingJobDto? job = null;
+            var playlist = state.OutputFilePath;
+
+            if (!System.IO.File.Exists(playlist))
+            {
+                var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
+                await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+                try
+                {
+                    if (!System.IO.File.Exists(playlist))
+                    {
+                        // If the playlist doesn't already exist, startup ffmpeg
+                        try
+                        {
+                            job = await _transcodingJobHelper.StartFfMpeg(
+                                    state,
+                                    playlist,
+                                    GetCommandLineArguments(playlist, state),
+                                    Request,
+                                    TranscodingJobType,
+                                    cancellationTokenSource)
+                                .ConfigureAwait(false);
+                            job.IsLiveOutput = true;
+                        }
+                        catch
+                        {
+                            state.Dispose();
+                            throw;
+                        }
+
+                        minSegments = state.MinSegments;
+                        if (minSegments > 0)
+                        {
+                            await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+                        }
+                    }
+                }
+                finally
+                {
+                    transcodingLock.Release();
+                }
+            }
+
+            job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+
+            if (job != null)
+            {
+                _transcodingJobHelper.OnTranscodeEndRequest(job);
+            }
+
+            var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
+
+            return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
+        }
+
+        /// <summary>
+        /// Gets the command line arguments for ffmpeg.
+        /// </summary>
+        /// <param name="outputPath">The output path of the file.</param>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <returns>The command line arguments as a string.</returns>
+        private string GetCommandLineArguments(string outputPath, StreamState state)
+        {
+            var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+            var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+            var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
+            var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
+            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+
+            var segmentFormat = format.TrimStart('.');
+            if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+            {
+                segmentFormat = "mpegts";
+            }
+
+            var baseUrlParam = string.Format(
+                CultureInfo.InvariantCulture,
+                "\"hls{0}\"",
+                Path.GetFileNameWithoutExtension(outputPath));
+
+            return string.Format(
+                    CultureInfo.InvariantCulture,
+                    "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
+                    inputModifier,
+                    _encodingHelper.GetInputArgument(state, _encodingOptions),
+                    threads,
+                    _encodingHelper.GetMapArgs(state),
+                    GetVideoArguments(state),
+                    GetAudioArguments(state),
+                    state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+                    string.Empty,
+                    segmentFormat,
+                    baseUrlParam,
+                    outputPath,
+                    outputTsArg)
+                .Trim();
+        }
+
+        /// <summary>
+        /// Gets the audio arguments for transcoding.
+        /// </summary>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <returns>The command line arguments for audio transcoding.</returns>
+        private string GetAudioArguments(StreamState state)
+        {
+            var codec = _encodingHelper.GetAudioEncoder(state);
+
+            if (EncodingHelper.IsCopyCodec(codec))
+            {
+                return "-codec:a:0 copy";
+            }
+
+            var args = "-codec:a:0 " + codec;
+
+            var channels = state.OutputAudioChannels;
+
+            if (channels.HasValue)
+            {
+                args += " -ac " + channels.Value;
+            }
+
+            var bitrate = state.OutputAudioBitrate;
+
+            if (bitrate.HasValue)
+            {
+                args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+            }
+
+            if (state.OutputAudioSampleRate.HasValue)
+            {
+                args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+            }
+
+            args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets the video arguments for transcoding.
+        /// </summary>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <returns>The command line arguments for video transcoding.</returns>
+        private string GetVideoArguments(StreamState state)
+        {
+            if (!state.IsOutputVideo)
+            {
+                return string.Empty;
+            }
+
+            var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+
+            var args = "-codec:v:0 " + codec;
+
+            // if (state.EnableMpegtsM2TsMode)
+            // {
+            //     args += " -mpegts_m2ts_mode 1";
+            // }
+
+            // See if we can save come cpu cycles by avoiding encoding
+            if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                // if h264_mp4toannexb is ever added, do not use it for live tv
+                if (state.VideoStream != null &&
+                    !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                {
+                    string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+                    if (!string.IsNullOrEmpty(bitStreamArgs))
+                    {
+                        args += " " + bitStreamArgs;
+                    }
+                }
+            }
+            else
+            {
+                var keyFrameArg = string.Format(
+                    CultureInfo.InvariantCulture,
+                    " -force_key_frames \"expr:gte(t,n_forced*{0})\"",
+                    state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+
+                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+                args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
+
+                // Add resolution params, if specified
+                if (!hasGraphicalSubs)
+                {
+                    args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+                }
+
+                // This is for internal graphical subs
+                if (hasGraphicalSubs)
+                {
+                    args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
+                }
+            }
+
+            args += " -flags -global_header";
+
+            if (!string.IsNullOrEmpty(state.OutputVideoSync))
+            {
+                args += " -vsync " + state.OutputVideoSync;
+            }
+
+            args += _encodingHelper.GetOutputFFlags(state);
+
+            return args;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
new file mode 100644
index 0000000000..2424966973
--- /dev/null
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// The hls helpers.
+    /// </summary>
+    public static class HlsHelpers
+    {
+        /// <summary>
+        /// Waits for a minimum number of segments to be available.
+        /// </summary>
+        /// <param name="playlist">The playlist string.</param>
+        /// <param name="segmentCount">The segment count.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <returns>A <see cref="Task"/> indicating the waiting process.</returns>
+        public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken)
+        {
+            logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
+
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                try
+                {
+                    // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
+                    var fileStream = new FileStream(
+                        playlist,
+                        FileMode.Open,
+                        FileAccess.Read,
+                        FileShare.ReadWrite,
+                        IODefaults.FileStreamBufferSize,
+                        FileOptions.SequentialScan);
+                    await using (fileStream.ConfigureAwait(false))
+                    {
+                        using var reader = new StreamReader(fileStream);
+                        var count = 0;
+
+                        while (!reader.EndOfStream)
+                        {
+                            var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                            if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+                            {
+                                count++;
+                                if (count >= segmentCount)
+                                {
+                                    logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+                                    return;
+                                }
+                            }
+                        }
+                    }
+
+                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                }
+                catch (IOException)
+                {
+                    // May get an error if the file is locked
+                }
+
+                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Gets the hls playlist text.
+        /// </summary>
+        /// <param name="path">The path to the playlist file.</param>
+        /// <param name="segmentLength">The segment length.</param>
+        /// <returns>The playlist text as a string.</returns>
+        public static string GetLivePlaylistText(string path, int segmentLength)
+        {
+            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+            using var reader = new StreamReader(stream);
+
+            var text = reader.ReadToEnd();
+
+            text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
+
+            var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
+
+            text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+            // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+
+            return text;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
index 9562f9953a..4487522c1b 100644
--- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
@@ -1,173 +1,6 @@
-using System;
-using System.Globalization;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
 namespace MediaBrowser.Api.Playback.Hls
 {
-    [Route("/Videos/{Id}/live.m3u8", "GET")]
     public class GetLiveHlsStream : VideoStreamRequest
     {
     }
-
-    /// <summary>
-    /// Class VideoHlsService.
-    /// </summary>
-    [Authenticated]
-    public class VideoHlsService : BaseHlsService
-    {
-        public VideoHlsService(
-            ILogger<VideoHlsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-        }
-
-        public Task<object> Get(GetLiveHlsStream request)
-        {
-            return ProcessRequestAsync(request, true);
-        }
-
-        /// <summary>
-        /// Gets the audio arguments.
-        /// </summary>
-        protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
-        {
-            var codec = EncodingHelper.GetAudioEncoder(state);
-
-            if (EncodingHelper.IsCopyCodec(codec))
-            {
-                return "-codec:a:0 copy";
-            }
-
-            var args = "-codec:a:0 " + codec;
-
-            var channels = state.OutputAudioChannels;
-
-            if (channels.HasValue)
-            {
-                args += " -ac " + channels.Value;
-            }
-
-            var bitrate = state.OutputAudioBitrate;
-
-            if (bitrate.HasValue)
-            {
-                args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            if (state.OutputAudioSampleRate.HasValue)
-            {
-                args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
-            return args;
-        }
-
-        /// <summary>
-        /// Gets the video arguments.
-        /// </summary>
-        protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
-        {
-            if (!state.IsOutputVideo)
-            {
-                return string.Empty;
-            }
-
-            var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-            var args = "-codec:v:0 " + codec;
-
-            // if (state.EnableMpegtsM2TsMode)
-            // {
-            //     args += " -mpegts_m2ts_mode 1";
-            // }
-
-            // See if we can save come cpu cycles by avoiding encoding
-            if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
-            {
-                // if h264_mp4toannexb is ever added, do not use it for live tv
-                if (state.VideoStream != null &&
-                    !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
-                {
-                    string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
-                    if (!string.IsNullOrEmpty(bitStreamArgs))
-                    {
-                        args += " " + bitStreamArgs;
-                    }
-                }
-            }
-            else
-            {
-                var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
-                    state.SegmentLength.ToString(CultureInfo.InvariantCulture));
-
-                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
-                args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg;
-
-                // Add resolution params, if specified
-                if (!hasGraphicalSubs)
-                {
-                    args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
-                }
-
-                // This is for internal graphical subs
-                if (hasGraphicalSubs)
-                {
-                    args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
-                }
-            }
-
-            args += " -flags -global_header";
-
-            if (!string.IsNullOrEmpty(state.OutputVideoSync))
-            {
-                args += " -vsync " + state.OutputVideoSync;
-            }
-
-            args += EncodingHelper.GetOutputFFlags(state);
-
-            return args;
-        }
-    }
 }

From c97372a1334bb18a6cd2b5a3abb0c385a30329ac Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 31 Jul 2020 09:21:33 -0600
Subject: [PATCH 368/463] Add missing docs and remove duplicate function

---
 Jellyfin.Api/Controllers/AudioController.cs  |  1 +
 Jellyfin.Api/Controllers/VideosController.cs |  1 +
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 14 --------------
 3 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 1d81def725..d9afbd9104 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -142,6 +142,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index fb02ec9617..d1ef817eb6 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -314,6 +314,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 16272c37a6..fc38eacafd 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -726,20 +726,6 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
-        /// <summary>
-        /// Transcoding video finished. Decrement the active request counter.
-        /// </summary>
-        /// <param name="job">The <see cref="TranscodingJobDto"/> which ended.</param>
-        public void OnTranscodeEndRequest(TranscodingJobDto job)
-        {
-            job.ActiveRequestCount--;
-            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
-            if (job.ActiveRequestCount <= 0)
-            {
-                PingTimer(job, false);
-            }
-        }
-
         /// <summary>
         /// Processes the exited.
         /// </summary>

From c994060794b63d20feb0bdd9bac6169f15592211 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 31 Jul 2020 17:23:52 +0200
Subject: [PATCH 369/463] Remove response code doc because it got added
 elsewhere

---
 Jellyfin.Api/Controllers/AudioController.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 9a789989b6..7405c26fb8 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -146,7 +146,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
         /// <param name="streamOptions">Optional. The streaming options.</param>
-        /// <response code="200">Audio file returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
         [HttpGet("{itemId}/{stream=stream}.{container?}")]
         [HttpGet("{itemId}/stream")]

From d6c428e998f624f8323ec019a3f80602694ff31e Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Fri, 31 Jul 2020 17:26:14 +0200
Subject: [PATCH 370/463] Update Jellyfin.Api/Controllers/VideoHlsController.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Api/Controllers/VideoHlsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index bd85a684ac..3f8a2048e4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="maxWidth">Optional. The max width.</param>
         /// <param name="maxHeight">Optional. The max height.</param>
         /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
-        /// <response code="200">Hls live stream retreaved.</response>
+        /// <response code="200">Hls live stream retrieved.</response>
         /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
         [HttpGet("/Videos/{itemId}/live.m3u8")]
         [ProducesResponseType(StatusCodes.Status200OK)]

From 6051df0c47876cd30ea0f39f8ef6ed1f54740a67 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 31 Jul 2020 10:14:09 -0600
Subject: [PATCH 371/463] Fix response codes, documentation, and auth

---
 Jellyfin.Api/Controllers/DlnaController.cs | 52 +++++++++++++---------
 1 file changed, 30 insertions(+), 22 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 68cd144f4e..dcd31a48ba 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -1,9 +1,8 @@
-#nullable enable
-
 using System.Collections.Generic;
+using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -12,7 +11,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Dlna Controller.
     /// </summary>
-    [Authenticated(Roles = "Admin")]
+    [Authorize(Policy = Policies.RequiresElevation)]
     public class DlnaController : BaseJellyfinApiController
     {
         private readonly IDlnaManager _dlnaManager;
@@ -29,34 +28,38 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get profile infos.
         /// </summary>
-        /// <returns>Profile infos.</returns>
+        /// <response code="200">Device profile infos returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
         [HttpGet("ProfileInfos")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IEnumerable<DeviceProfileInfo> GetProfileInfos()
+        public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
         {
-            return _dlnaManager.GetProfileInfos();
+            return Ok(_dlnaManager.GetProfileInfos());
         }
 
         /// <summary>
         /// Gets the default profile.
         /// </summary>
-        /// <returns>Default profile.</returns>
+        /// <response code="200">Default device profile returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
         [HttpGet("Profiles/Default")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<DeviceProfileInfo> GetDefaultProfile()
+        public ActionResult<DeviceProfile> GetDefaultProfile()
         {
-            return Ok(_dlnaManager.GetDefaultProfile());
+            return _dlnaManager.GetDefaultProfile();
         }
 
         /// <summary>
         /// Gets a single profile.
         /// </summary>
         /// <param name="id">Profile Id.</param>
-        /// <returns>Profile.</returns>
+        /// <response code="200">Device profile returned.</response>
+        /// <response code="404">Device profile not found.</response>
+        /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
         [HttpGet("Profiles/{Id}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceProfileInfo> GetProfile([FromRoute] string id)
+        public ActionResult<DeviceProfile> GetProfile([FromRoute] string id)
         {
             var profile = _dlnaManager.GetProfile(id);
             if (profile == null)
@@ -64,16 +67,18 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
-            return Ok(profile);
+            return profile;
         }
 
         /// <summary>
         /// Deletes a profile.
         /// </summary>
         /// <param name="id">Profile id.</param>
-        /// <returns>Status.</returns>
+        /// <response code="204">Device profile deleted.</response>
+        /// <response code="404">Device profile not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
         [HttpDelete("Profiles/{Id}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult DeleteProfile([FromRoute] string id)
         {
@@ -84,20 +89,21 @@ namespace Jellyfin.Api.Controllers
             }
 
             _dlnaManager.DeleteProfile(id);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
         /// Creates a profile.
         /// </summary>
         /// <param name="deviceProfile">Device profile.</param>
-        /// <returns>Status.</returns>
+        /// <response code="204">Device profile created.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Profiles")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
         {
             _dlnaManager.CreateProfile(deviceProfile);
-            return Ok();
+            return NoContent();
         }
 
         /// <summary>
@@ -105,9 +111,11 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="id">Profile id.</param>
         /// <param name="deviceProfile">Device profile.</param>
-        /// <returns>Status.</returns>
+        /// <response code="204">Device profile updated.</response>
+        /// <response code="404">Device profile not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
         [HttpPost("Profiles/{Id}")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateProfile([FromRoute] string id, [FromBody] DeviceProfile deviceProfile)
         {
@@ -118,7 +126,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             _dlnaManager.UpdateProfile(deviceProfile);
-            return Ok();
+            return NoContent();
         }
     }
 }

From 32c0ac96a14bcc42e57f9583d71dd65c804bb577 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 31 Jul 2020 10:17:01 -0600
Subject: [PATCH 372/463] fix route params

---
 Jellyfin.Api/Controllers/DlnaController.cs | 26 +++++++++++-----------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index dcd31a48ba..397299a73f 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -52,16 +52,16 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets a single profile.
         /// </summary>
-        /// <param name="id">Profile Id.</param>
+        /// <param name="profileId">Profile Id.</param>
         /// <response code="200">Device profile returned.</response>
         /// <response code="404">Device profile not found.</response>
         /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
-        [HttpGet("Profiles/{Id}")]
+        [HttpGet("Profiles/{profileId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceProfile> GetProfile([FromRoute] string id)
+        public ActionResult<DeviceProfile> GetProfile([FromRoute] string profileId)
         {
-            var profile = _dlnaManager.GetProfile(id);
+            var profile = _dlnaManager.GetProfile(profileId);
             if (profile == null)
             {
                 return NotFound();
@@ -73,22 +73,22 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Deletes a profile.
         /// </summary>
-        /// <param name="id">Profile id.</param>
+        /// <param name="profileId">Profile id.</param>
         /// <response code="204">Device profile deleted.</response>
         /// <response code="404">Device profile not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
-        [HttpDelete("Profiles/{Id}")]
+        [HttpDelete("Profiles/{profileId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult DeleteProfile([FromRoute] string id)
+        public ActionResult DeleteProfile([FromRoute] string profileId)
         {
-            var existingDeviceProfile = _dlnaManager.GetProfile(id);
+            var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
             if (existingDeviceProfile == null)
             {
                 return NotFound();
             }
 
-            _dlnaManager.DeleteProfile(id);
+            _dlnaManager.DeleteProfile(profileId);
             return NoContent();
         }
 
@@ -109,17 +109,17 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Updates a profile.
         /// </summary>
-        /// <param name="id">Profile id.</param>
+        /// <param name="profileId">Profile id.</param>
         /// <param name="deviceProfile">Device profile.</param>
         /// <response code="204">Device profile updated.</response>
         /// <response code="404">Device profile not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
-        [HttpPost("Profiles/{Id}")]
+        [HttpPost("Profiles/{profileId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateProfile([FromRoute] string id, [FromBody] DeviceProfile deviceProfile)
+        public ActionResult UpdateProfile([FromRoute] string profileId, [FromBody] DeviceProfile deviceProfile)
         {
-            var existingDeviceProfile = _dlnaManager.GetProfile(id);
+            var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
             if (existingDeviceProfile == null)
             {
                 return NotFound();

From 4bb1e1c29246e9293ee119e85c41edef51c39972 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 31 Jul 2020 11:01:30 -0600
Subject: [PATCH 373/463] Fix docs, params, return values

---
 .../Controllers/DlnaServerController.cs       | 140 +++++++++---------
 1 file changed, 69 insertions(+), 71 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 731d6707cf..2f5561adb9 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -1,6 +1,5 @@
-#nullable enable
-
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Threading.Tasks;
 using Emby.Dlna;
@@ -10,8 +9,6 @@ using MediaBrowser.Controller.Dlna;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
-#pragma warning disable CA1801
-
 namespace Jellyfin.Api.Controllers
 {
     /// <summary>
@@ -42,37 +39,33 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Get Description Xml.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
-        /// <returns>Description Xml.</returns>
-        [HttpGet("{Uuid}/description.xml")]
-        [HttpGet("{Uuid}/description")]
+        /// <param name="serverId">Server UUID.</param>
+        /// <response code="200">Description xml returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
+        [HttpGet("{serverId}/description.xml")]
+        [HttpGet("{serverId}/description")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult GetDescriptionXml([FromRoute] string uuid)
+        public ActionResult GetDescriptionXml([FromRoute] string serverId)
         {
             var url = GetAbsoluteUri();
             var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
-            var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, uuid, serverAddress);
-
-            // TODO GetStaticResult doesn't do anything special?
-            /*
-            var cacheLength = TimeSpan.FromDays(1);
-            var cacheKey = Request.Path.Value.GetMD5();
-            var bytes = Encoding.UTF8.GetBytes(xml);
-            */
+            var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
             return Ok(xml);
         }
 
         /// <summary>
         /// Gets Dlna content directory xml.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
-        /// <returns>Dlna content directory xml.</returns>
-        [HttpGet("{Uuid}/ContentDirectory/ContentDirectory.xml")]
-        [HttpGet("{Uuid}/ContentDirectory/ContentDirectory")]
+        /// <param name="serverId">Server UUID.</param>
+        /// <response code="200">Dlna content directory returned.</response>
+        /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult GetContentDirectory([FromRoute] string uuid)
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult GetContentDirectory([FromRoute] string serverId)
         {
             return Ok(_contentDirectory.GetServiceXml());
         }
@@ -80,13 +73,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets Dlna media receiver registrar xml.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
-        [HttpGet("{Uuid}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult GetMediaReceiverRegistrar([FromRoute] string uuid)
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
         {
             return Ok(_mediaReceiverRegistrar.GetServiceXml());
         }
@@ -94,13 +88,14 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets Dlna media receiver registrar xml.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{Uuid}/ConnectionManager/ConnectionManager.xml")]
-        [HttpGet("{Uuid}/ConnectionManager/ConnectionManager")]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult GetConnectionManager([FromRoute] string uuid)
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult GetConnectionManager([FromRoute] string serverId)
         {
             return Ok(_connectionManager.GetServiceXml());
         }
@@ -108,100 +103,103 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Process a content directory control request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Control response.</returns>
-        [HttpPost("{Uuid}/ContentDirectory/Control")]
-        public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string uuid)
+        [HttpPost("{serverId}/ContentDirectory/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string serverId)
         {
-            var response = await PostAsync(uuid, Request.Body, _contentDirectory).ConfigureAwait(false);
-            return Ok(response);
+            return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Process a connection manager control request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Control response.</returns>
-        [HttpPost("{Uuid}/ConnectionManager/Control")]
-        public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string uuid)
+        [HttpPost("{serverId}/ConnectionManager/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string serverId)
         {
-            var response = await PostAsync(uuid, Request.Body, _connectionManager).ConfigureAwait(false);
-            return Ok(response);
+            return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Process a media receiver registrar control request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Control response.</returns>
-        [HttpPost("{Uuid}/MediaReceiverRegistrar/Control")]
-        public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string uuid)
+        [HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
+        public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
         {
-            var response = await PostAsync(uuid, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
-            return Ok(response);
+            return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Processes an event subscription request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Event subscription response.</returns>
-        [HttpSubscribe("{Uuid}/MediaReceiverRegistrar/Events")]
-        [HttpUnsubscribe("{Uuid}/MediaReceiverRegistrar/Events")]
-        public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string uuid)
+        [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
+        [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
+        [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
         {
-            return Ok(ProcessEventRequest(_mediaReceiverRegistrar));
+            return ProcessEventRequest(_mediaReceiverRegistrar);
         }
 
         /// <summary>
         /// Processes an event subscription request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Event subscription response.</returns>
-        [HttpSubscribe("{Uuid}/ContentDirectory/Events")]
-        [HttpUnsubscribe("{Uuid}/ContentDirectory/Events")]
-        public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string uuid)
+        [HttpSubscribe("{serverId}/ContentDirectory/Events")]
+        [HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
+        [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
         {
-            return Ok(ProcessEventRequest(_contentDirectory));
+            return ProcessEventRequest(_contentDirectory);
         }
 
         /// <summary>
         /// Processes an event subscription request.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <returns>Event subscription response.</returns>
-        [HttpSubscribe("{Uuid}/ConnectionManager/Events")]
-        [HttpUnsubscribe("{Uuid}/ConnectionManager/Events")]
-        public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string uuid)
+        [HttpSubscribe("{serverId}/ConnectionManager/Events")]
+        [HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
+        [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
         {
-            return Ok(ProcessEventRequest(_connectionManager));
+            return ProcessEventRequest(_connectionManager);
         }
 
         /// <summary>
         /// Gets a server icon.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
+        /// <param name="serverId">Server UUID.</param>
         /// <param name="fileName">The icon filename.</param>
         /// <returns>Icon stream.</returns>
-        [HttpGet("{Uuid}/icons/{Filename}")]
-        public ActionResult<FileStreamResult> GetIconId([FromRoute] string uuid, [FromRoute] string fileName)
+        [HttpGet("{serverId}/icons/{filename}")]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+        public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
         {
-            return GetIcon(fileName);
+            return GetIconInternal(fileName);
         }
 
         /// <summary>
         /// Gets a server icon.
         /// </summary>
-        /// <param name="uuid">Server UUID.</param>
         /// <param name="fileName">The icon filename.</param>
         /// <returns>Icon stream.</returns>
-        [HttpGet("icons/{Filename}")]
-        public ActionResult<FileStreamResult> GetIcon([FromQuery] string uuid, [FromRoute] string fileName)
+        [HttpGet("icons/{filename}")]
+        public ActionResult GetIcon([FromRoute] string fileName)
         {
-            return GetIcon(fileName);
+            return GetIconInternal(fileName);
         }
 
-        private ActionResult<FileStreamResult> GetIcon(string fileName)
+        private ActionResult GetIconInternal(string fileName)
         {
             var icon = _dlnaManager.GetIcon(fileName);
             if (icon == null)
@@ -213,7 +211,7 @@ namespace Jellyfin.Api.Controllers
                 .TrimStart('.')
                 .ToLowerInvariant();
 
-            return new FileStreamResult(icon.Stream, contentType);
+            return File(icon.Stream, contentType);
         }
 
         private string GetAbsoluteUri()
@@ -221,7 +219,7 @@ namespace Jellyfin.Api.Controllers
             return $"{Request.Scheme}://{Request.Host}{Request.Path}";
         }
 
-        private Task<ControlResponse> PostAsync(string id, Stream requestStream, IUpnpService service)
+        private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
         {
             return service.ProcessControlRequestAsync(new ControlRequest
             {

From e0d2eb8eec1d48175c30309e1e4c0a771329ff4b Mon Sep 17 00:00:00 2001
From: dkanada <dkanada@users.noreply.github.com>
Date: Sat, 1 Aug 2020 02:03:23 +0900
Subject: [PATCH 374/463] remove useless order step for intros

---
 Emby.Server.Implementations/Library/LibraryManager.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index f3c2f0e035..169f50b646 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1592,7 +1592,6 @@ namespace Emby.Server.Implementations.Library
         public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
         {
             var tasks = IntroProviders
-                .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
                 .Take(1)
                 .Select(i => GetIntros(i, item, user));
 

From 7ce99aaf785d3567633febfb60de1b26c43f7d4d Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Fri, 31 Jul 2020 21:20:05 +0200
Subject: [PATCH 375/463] Update SkiaSharp to 2.80.1 and replace resize code.

This fixed the blurry resized images in the Web UI.
---
 .../Jellyfin.Drawing.Skia.csproj              |  5 +--
 Jellyfin.Drawing.Skia/SkiaEncoder.cs          | 43 +++++++++++++++++--
 Jellyfin.Drawing.Skia/StripCollageBuilder.cs  | 13 +++---
 3 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 65a4394594..c71c76f08f 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -20,9 +20,8 @@
   <ItemGroup>
     <PackageReference Include="BlurHashSharp" Version="1.1.0" />
     <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
-    <PackageReference Include="SkiaSharp" Version="1.68.3" />
-    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
-    <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
+    <PackageReference Include="SkiaSharp" Version="2.80.1" />
+    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 1f7c43de81..a66ecc0813 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -395,6 +395,42 @@ namespace Jellyfin.Drawing.Skia
             return rotated;
         }
 
+        /// <summary>
+        /// Resizes an image on the CPU, by utilizing a surface and canvas.
+        /// </summary>
+        /// <param name="source">The source bitmap.</param>
+        /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
+        /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
+        /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
+        /// <returns>The resized image.</returns>
+        internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
+        {
+            using var surface = SKSurface.Create(targetInfo);
+            using var canvas = surface.Canvas;
+            using var paint = new SKPaint();
+
+            paint.FilterQuality = SKFilterQuality.High;
+            paint.IsAntialias = isAntialias;
+            paint.IsDither = isDither;
+
+            var kernel = new float[9]
+            {
+                        0,    -.1f,    0,
+                        -.1f, 1.4f, -.1f,
+                        0,    -.1f,    0,
+            };
+
+            var kernelSize = new SKSizeI(3, 3);
+            var kernelOffset = new SKPointI(1, 1);
+
+            paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
+                kernelSize, kernel, 1f, 0f, kernelOffset, SKShaderTileMode.Clamp, false);
+
+            canvas.DrawBitmap(source, SKRect.Create(0, 0, source.Width, source.Height), SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), paint);
+
+            return surface.Snapshot();
+        }
+
         /// <inheritdoc/>
         public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
         {
@@ -436,9 +472,8 @@ namespace Jellyfin.Drawing.Skia
             var width = newImageSize.Width;
             var height = newImageSize.Height;
 
-            using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType);
-            // scale image
-            bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+            // scale image (the FromImage creates a copy)
+            using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace)));
 
             // If all we're doing is resizing then we can stop now
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
@@ -446,7 +481,7 @@ namespace Jellyfin.Drawing.Skia
                 Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                 using var outputStream = new SKFileWStream(outputPath);
                 using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
-                pixmap.Encode(outputStream, skiaOutputFormat, quality);
+                resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
                 return outputPath;
             }
 
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index e0ee4a342d..3b35594865 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -115,15 +115,13 @@ namespace Jellyfin.Drawing.Skia
 
                 // resize to the same aspect as the original
                 int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
-                using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
-                currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
+                using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
 
                 // crop image
                 int ix = Math.Abs((iWidth - iSlice) / 2);
-                using var image = SKImage.FromBitmap(resizeBitmap);
-                using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
+                using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
                 // draw image onto canvas
-                canvas.DrawImage(subset ?? image, iSlice * i, 0);
+                canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
             }
 
             return bitmap;
@@ -177,9 +175,8 @@ namespace Jellyfin.Drawing.Skia
                         continue;
                     }
 
-                    using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
-                    // scale image
-                    currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+                    // Scale image. The FromBitmap creates a copy
+                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace)));
 
                     // draw this image into the strip at the next position
                     var xPos = x * cellWidth;

From af38e5469f30b1b08e341fa00a7a7eea53388b8a Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Fri, 31 Jul 2020 21:33:25 +0200
Subject: [PATCH 376/463] Update Jellyfin.Drawing.Skia/SkiaEncoder.cs
 indentation.

Co-authored-by: Cody Robibero <cody@robibe.ro>
---
 Jellyfin.Drawing.Skia/SkiaEncoder.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index a66ecc0813..4e08758f62 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -415,9 +415,9 @@ namespace Jellyfin.Drawing.Skia
 
             var kernel = new float[9]
             {
-                        0,    -.1f,    0,
-                        -.1f, 1.4f, -.1f,
-                        0,    -.1f,    0,
+                0,    -.1f,    0,
+                -.1f, 1.4f, -.1f,
+                0,    -.1f,    0,
             };
 
             var kernelSize = new SKSizeI(3, 3);

From a6d80f557df8587c178c1a65722c1aa968a051e7 Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Fri, 31 Jul 2020 21:40:09 +0200
Subject: [PATCH 377/463] Add a much shorter timeout to the CollectArtifacts
 job

---
 .ci/azure-pipelines-package.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 1677f71c75..10ce8508df 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -116,6 +116,7 @@ jobs:
         $(JellyfinVersion)-$(BuildConfiguration)
 
 - job: CollectArtifacts
+  timeoutInMinutes: 10
   displayName: 'Collect Artifacts'
   dependsOn:
   - BuildPackage

From 526eea41f0b74c5717575887839ecc41ca22067f Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Fri, 31 Jul 2020 22:02:16 +0200
Subject: [PATCH 378/463] Add a note on the convolutional matrix filter.

---
 Jellyfin.Drawing.Skia/SkiaEncoder.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 4e08758f62..58d3039557 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -397,6 +397,9 @@ namespace Jellyfin.Drawing.Skia
 
         /// <summary>
         /// Resizes an image on the CPU, by utilizing a surface and canvas.
+        ///
+        /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
+        /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
         /// </summary>
         /// <param name="source">The source bitmap.</param>
         /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>

From 44aca4dc6ff478776ca56c95763b3db7177234f3 Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Fri, 31 Jul 2020 22:12:20 +0200
Subject: [PATCH 379/463] Formatting in SkiaEncoder.cs

---
 Jellyfin.Drawing.Skia/SkiaEncoder.cs | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 58d3039557..62e1d6ed14 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -410,11 +410,12 @@ namespace Jellyfin.Drawing.Skia
         {
             using var surface = SKSurface.Create(targetInfo);
             using var canvas = surface.Canvas;
-            using var paint = new SKPaint();
-
-            paint.FilterQuality = SKFilterQuality.High;
-            paint.IsAntialias = isAntialias;
-            paint.IsDither = isDither;
+            using var paint = new SKPaint
+            {
+                FilterQuality = SKFilterQuality.High,
+                IsAntialias = isAntialias,
+                IsDither = isDither
+            };
 
             var kernel = new float[9]
             {
@@ -427,9 +428,19 @@ namespace Jellyfin.Drawing.Skia
             var kernelOffset = new SKPointI(1, 1);
 
             paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
-                kernelSize, kernel, 1f, 0f, kernelOffset, SKShaderTileMode.Clamp, false);
-
-            canvas.DrawBitmap(source, SKRect.Create(0, 0, source.Width, source.Height), SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), paint);
+                kernelSize,
+                kernel,
+                1f,
+                0f,
+                kernelOffset,
+                SKShaderTileMode.Clamp,
+                false);
+
+            canvas.DrawBitmap(
+                source,
+                SKRect.Create(0, 0, source.Width, source.Height),
+                SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+                paint);
 
             return surface.Snapshot();
         }

From f645e2f8841577d7f1cd3a664d401c8c1837cca0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 31 Jul 2020 15:09:17 -0600
Subject: [PATCH 380/463] Move DynamicHlsService to Jellyfin.Api

---
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs |    7 +-
 .../Controllers/DynamicHlsController.cs       | 2216 +++++++++++++++++
 Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs |  125 +
 Jellyfin.Api/Helpers/RequestHelpers.cs        |    6 +
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |    6 +-
 .../StreamingDtos/HlsAudioRequestDto.cs       |   13 +
 .../StreamingDtos/HlsVideoRequestDto.cs       |   13 +
 .../Playback/Hls/DynamicHlsService.cs         |    8 -
 8 files changed, 2377 insertions(+), 17 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/DynamicHlsController.cs
 create mode 100644 Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
 create mode 100644 Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
 create mode 100644 Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs

diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 9fde175d0b..495ff9d128 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -70,7 +70,7 @@ namespace Jellyfin.Api.Auth
                 return false;
             }
 
-            var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+            var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
             var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
             // User cannot access remotely and user is remote
             if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
@@ -100,10 +100,5 @@ namespace Jellyfin.Api.Auth
 
             return true;
         }
-
-        private static IPAddress NormalizeIp(IPAddress ip)
-        {
-            return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
-        }
     }
 }
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
new file mode 100644
index 0000000000..c7b84d810d
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -0,0 +1,2216 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// Dynamic hls controller.
+    /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
+    public class DynamicHlsController : BaseJellyfinApiController
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly IUserManager _userManager;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IAuthorizationContext _authContext;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IConfiguration _configuration;
+        private readonly IDeviceManager _deviceManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly INetworkManager _networkManager;
+        private readonly ILogger<DynamicHlsController> _logger;
+        private readonly EncodingHelper _encodingHelper;
+
+        private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
+        /// </summary>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+        public DynamicHlsController(
+            ILibraryManager libraryManager,
+            IUserManager userManager,
+            IDlnaManager dlnaManager,
+            IAuthorizationContext authContext,
+            IMediaSourceManager mediaSourceManager,
+            IServerConfigurationManager serverConfigurationManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            ISubtitleEncoder subtitleEncoder,
+            IConfiguration configuration,
+            IDeviceManager deviceManager,
+            TranscodingJobHelper transcodingJobHelper,
+            INetworkManager networkManager,
+            ILogger<DynamicHlsController> logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _dlnaManager = dlnaManager;
+            _authContext = authContext;
+            _mediaSourceManager = mediaSourceManager;
+            _serverConfigurationManager = serverConfigurationManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _subtitleEncoder = subtitleEncoder;
+            _configuration = configuration;
+            _deviceManager = deviceManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _networkManager = networkManager;
+            _logger = logger;
+
+            _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+        }
+
+        /// <summary>
+        /// Gets a video hls playlist stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+        [HttpGet("/Videos/{itemId}/master.m3u8")]
+        [HttpHead("/Videos/{itemId}/master.m3u8")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetMasterHlsVideoPlaylist(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery, Required] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions,
+            [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+        {
+            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new HlsVideoRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions,
+                EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+            };
+
+            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets an audio hls playlist stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+        /// <response code="200">Audio stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+        [HttpGet("/Audio/{itemId}/master.m3u8")]
+        [HttpHead("/Audio/{itemId}/master.m3u8")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetMasterHlsAudioPlaylist(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery, Required] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions,
+            [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+        {
+            var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new HlsAudioRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions,
+                EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+            };
+
+            return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets a video stream using HTTP live streaming.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("/Videos/{itemId}/main.m3u8")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetVariantHlsVideoPlaylist(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new VideoRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets an audio stream using HTTP live streaming.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Audio stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("/Audio/{itemId}/main.m3u8")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ActionResult> GetVariantHlsAudioPlaylist(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var cancellationTokenSource = new CancellationTokenSource();
+            var streamingRequest = new StreamingRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets a video stream using HTTP live streaming.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="segmentId">The segment id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+        public async Task<ActionResult> GetHlsVideoSegment(
+            [FromRoute] Guid itemId,
+            [FromRoute] string playlistId,
+            [FromRoute] int segmentId,
+            [FromRoute] string container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var streamingRequest = new VideoRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            return await GetDynamicSegment(streamingRequest, segmentId)
+                .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets a video stream using HTTP live streaming.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="playlistId">The playlist id.</param>
+        /// <param name="segmentId">The segment id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("/Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+        public async Task<ActionResult> GetHlsAudioSegment(
+            [FromRoute] Guid itemId,
+            [FromRoute] string playlistId,
+            [FromRoute] int segmentId,
+            [FromRoute] string container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            var streamingRequest = new StreamingRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context,
+                StreamOptions = streamOptions
+            };
+
+            return await GetDynamicSegment(streamingRequest, segmentId)
+                .ConfigureAwait(false);
+        }
+
+        private async Task<ActionResult> GetMasterPlaylistInternal(
+            StreamingRequestDto streamingRequest,
+            bool isHeadRequest,
+            bool enableAdaptiveBitrateStreaming,
+            CancellationTokenSource cancellationTokenSource)
+        {
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            Response.Headers.Add(HeaderNames.Expires, "0");
+            if (isHeadRequest)
+            {
+                return new FileContentResult(Encoding.UTF8.GetBytes(string.Empty), MimeTypes.GetMimeType("playlist.m3u8"));
+            }
+
+            var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+
+            var isLiveStream = state.IsSegmentedLiveStream;
+
+            var queryString = Request.Query.ToString();
+
+            // from universal audio service
+            if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+            {
+                queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
+            }
+
+            // from universal audio service
+            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+            {
+                queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
+            }
+
+            // Main stream
+            var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+
+            playlistUrl += queryString;
+
+            var subtitleStreams = state.MediaSource
+                .MediaStreams
+                .Where(i => i.IsTextSubtitleStream)
+                .ToList();
+
+            var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
+                ? "subs"
+                : null;
+
+            // If we're burning in subtitles then don't add additional subs to the manifest
+            if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            {
+                subtitleGroup = null;
+            }
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                AddSubtitles(state, subtitleStreams, builder);
+            }
+
+            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+            if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
+            {
+                var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
+
+                // By default, vary by just 200k
+                var variation = GetBitrateVariation(totalBitrate);
+
+                var newBitrate = totalBitrate - variation;
+                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+
+                variation *= 2;
+                newBitrate = totalBitrate - variation;
+                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
+            }
+
+            return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+        }
+
+        private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
+        {
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            Response.Headers.Add(HeaderNames.Expires, "0");
+
+            var segmentLengths = GetSegmentLengths(state);
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+            var queryString = Request.QueryString;
+            var index = 0;
+
+            var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+
+            foreach (var length in segmentLengths)
+            {
+                builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
+                builder.AppendLine(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "hls1/{0}/{1}{2}{3}",
+                        name,
+                        index.ToString(CultureInfo.InvariantCulture),
+                        segmentExtension,
+                        queryString));
+
+                index++;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+            return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+        }
+
+        private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
+        {
+            if ((streamingRequest.StartTimeTicks ?? 0) > 0)
+            {
+                throw new ArgumentException("StartTimeTicks is not allowed.");
+            }
+
+            var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationToken = cancellationTokenSource.Token;
+
+            using var state = await StreamingHelpers.GetStreamingState(
+                    streamingRequest,
+                    Request,
+                    _authContext,
+                    _mediaSourceManager,
+                    _userManager,
+                    _libraryManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _dlnaManager,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _transcodingJobType,
+                    cancellationTokenSource.Token)
+                .ConfigureAwait(false);
+
+            var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+
+            var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
+
+            var segmentExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
+
+            TranscodingJobDto? job;
+
+            if (System.IO.File.Exists(segmentPath))
+            {
+                job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+                _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
+                return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+            }
+
+            var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+            await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            var released = false;
+            var startTranscoding = false;
+
+            try
+            {
+                if (System.IO.File.Exists(segmentPath))
+                {
+                    job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+                    transcodingLock.Release();
+                    released = true;
+                    _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
+                    return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+                    var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+                    if (currentTranscodingIndex == null)
+                    {
+                        _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
+                        startTranscoding = true;
+                    }
+                    else if (segmentId < currentTranscodingIndex.Value)
+                    {
+                        _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
+                        startTranscoding = true;
+                    }
+                    else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
+                    {
+                        _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
+                        startTranscoding = true;
+                    }
+
+                    if (startTranscoding)
+                    {
+                        // If the playlist doesn't already exist, startup ffmpeg
+                        try
+                        {
+                            await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+                                .ConfigureAwait(false);
+
+                            if (currentTranscodingIndex.HasValue)
+                            {
+                                DeleteLastFile(playlistPath, segmentExtension, 0);
+                            }
+
+                            streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
+
+                            state.WaitForPath = segmentPath;
+                            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+                            job = await _transcodingJobHelper.StartFfMpeg(
+                                state,
+                                playlistPath,
+                                GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
+                                Request,
+                                _transcodingJobType,
+                                cancellationTokenSource).ConfigureAwait(false);
+                        }
+                        catch
+                        {
+                            state.Dispose();
+                            throw;
+                        }
+
+                        // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+                        if (job?.TranscodingThrottler != null)
+                        {
+                            await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
+                        }
+                    }
+                }
+            }
+            finally
+            {
+                if (!released)
+                {
+                    transcodingLock.Release();
+                }
+            }
+
+            _logger.LogDebug("returning {0} [general case]", segmentPath);
+            job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+            return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+        }
+
+        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
+        {
+            var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
+
+            foreach (var stream in subtitles)
+            {
+                const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
+
+                var name = stream.DisplayTitle;
+
+                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
+                var isForced = stream.IsForced;
+
+                var url = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+                    state.Request.MediaSourceId,
+                    stream.Index.ToString(CultureInfo.InvariantCulture),
+                    30.ToString(CultureInfo.InvariantCulture),
+                    ClaimHelpers.GetToken(Request.HttpContext.User));
+
+                var line = string.Format(
+                    CultureInfo.InvariantCulture,
+                    format,
+                    name,
+                    isDefault ? "YES" : "NO",
+                    isForced ? "YES" : "NO",
+                    url,
+                    stream.Language ?? "Unknown");
+
+                builder.AppendLine(line);
+            }
+        }
+
+        private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+        {
+            builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+                .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+                .Append(",AVERAGE-BANDWIDTH=")
+                .Append(bitrate.ToString(CultureInfo.InvariantCulture));
+
+            AppendPlaylistCodecsField(builder, state);
+
+            AppendPlaylistResolutionField(builder, state);
+
+            AppendPlaylistFramerateField(builder, state);
+
+            if (!string.IsNullOrWhiteSpace(subtitleGroup))
+            {
+                builder.Append(",SUBTITLES=\"")
+                    .Append(subtitleGroup)
+                    .Append('"');
+            }
+
+            builder.Append(Environment.NewLine);
+            builder.AppendLine(url);
+        }
+
+        /// <summary>
+        /// Appends a CODECS field containing formatted strings of
+        /// the active streams output video and audio codecs.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+        {
+            // Video
+            string videoCodecs = string.Empty;
+            int? videoCodecLevel = GetOutputVideoCodecLevel(state);
+            if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
+            {
+                videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+            }
+
+            // Audio
+            string audioCodecs = string.Empty;
+            if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+            {
+                audioCodecs = GetPlaylistAudioCodecs(state);
+            }
+
+            StringBuilder codecs = new StringBuilder();
+
+            codecs.Append(videoCodecs)
+                .Append(',')
+                .Append(audioCodecs);
+
+            if (codecs.Length > 1)
+            {
+                builder.Append(",CODECS=\"")
+                    .Append(codecs)
+                    .Append('"');
+            }
+        }
+
+        /// <summary>
+        /// Appends a RESOLUTION field containing the resolution of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+        {
+            if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
+            {
+                builder.Append(",RESOLUTION=")
+                    .Append(state.OutputWidth.GetValueOrDefault())
+                    .Append('x')
+                    .Append(state.OutputHeight.GetValueOrDefault());
+            }
+        }
+
+        /// <summary>
+        /// Appends a FRAME-RATE field containing the framerate of the output stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+        {
+            double? framerate = null;
+            if (state.TargetFramerate.HasValue)
+            {
+                framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
+            }
+            else if (state.VideoStream?.RealFrameRate != null)
+            {
+                framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
+            }
+
+            if (framerate.HasValue)
+            {
+                builder.Append(",FRAME-RATE=")
+                    .Append(framerate.Value);
+            }
+        }
+
+        private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming)
+        {
+            // Within the local network this will likely do more harm than good.
+            var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString();
+            if (_networkManager.IsInLocalNetwork(ip))
+            {
+                return false;
+            }
+
+            if (!enableAdaptiveBitrateStreaming)
+            {
+                return false;
+            }
+
+            if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
+            {
+                // Opening live streams is so slow it's not even worth it
+                return false;
+            }
+
+            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            {
+                return false;
+            }
+
+            if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+            {
+                return false;
+            }
+
+            if (!state.IsOutputVideo)
+            {
+                return false;
+            }
+
+            // Having problems in android
+            return false;
+            // return state.VideoRequest.VideoBitRate.HasValue;
+        }
+
+        /// <summary>
+        /// Get the H.26X level of the output video stream.
+        /// </summary>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>H.26X level of the output video stream.</returns>
+        private int? GetOutputVideoCodecLevel(StreamState state)
+        {
+            string? levelString;
+            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+                && state.VideoStream.Level.HasValue)
+            {
+                levelString = state.VideoStream?.Level.ToString();
+            }
+            else
+            {
+                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+            }
+
+            if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+            {
+                return parsedLevel;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>Formatted audio codec string.</returns>
+        private string GetPlaylistAudioCodecs(StreamState state)
+        {
+            if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+            {
+                string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+                return HlsCodecStringHelpers.GetAACString(profile);
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetMP3String();
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetAC3String();
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetEAC3String();
+            }
+
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output video codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <param name="codec">Video codec.</param>
+        /// <param name="level">Video level.</param>
+        /// <returns>Formatted video codec string.</returns>
+        private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+        {
+            if (level == 0)
+            {
+                // This is 0 when there's no requested H.26X level in the device profile
+                // and the source is not encoded in H.26X
+                _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
+                return string.Empty;
+            }
+
+            if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+                return HlsCodecStringHelpers.GetH264String(profile, level);
+            }
+
+            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+
+                return HlsCodecStringHelpers.GetH265String(profile, level);
+            }
+
+            return string.Empty;
+        }
+
+        private int GetBitrateVariation(int bitrate)
+        {
+            // By default, vary by just 50k
+            var variation = 50000;
+
+            if (bitrate >= 10000000)
+            {
+                variation = 2000000;
+            }
+            else if (bitrate >= 5000000)
+            {
+                variation = 1500000;
+            }
+            else if (bitrate >= 3000000)
+            {
+                variation = 1000000;
+            }
+            else if (bitrate >= 2000000)
+            {
+                variation = 500000;
+            }
+            else if (bitrate >= 1000000)
+            {
+                variation = 300000;
+            }
+            else if (bitrate >= 600000)
+            {
+                variation = 200000;
+            }
+            else if (bitrate >= 400000)
+            {
+                variation = 100000;
+            }
+
+            return variation;
+        }
+
+        private string ReplaceBitrate(string url, int oldValue, int newValue)
+        {
+            return url.Replace(
+                "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
+                "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
+                StringComparison.OrdinalIgnoreCase);
+        }
+
+        private double[] GetSegmentLengths(StreamState state)
+        {
+            var result = new List<double>();
+
+            var ticks = state.RunTimeTicks ?? 0;
+
+            var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
+
+            while (ticks > 0)
+            {
+                var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
+
+                result.Add(TimeSpan.FromTicks(length).TotalSeconds);
+
+                ticks -= length;
+            }
+
+            return result.ToArray();
+        }
+
+        private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
+        {
+            var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+            var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
+
+            if (state.BaseRequest.BreakOnNonKeyFrames)
+            {
+                // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
+                //        breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
+                //        to produce a missing part of video stream before first keyframe is encountered, which may lead to
+                //        awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
+                _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
+                state.BaseRequest.BreakOnNonKeyFrames = false;
+            }
+
+            var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
+
+            // If isEncoding is true we're actually starting ffmpeg
+            var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
+
+            var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+
+            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
+
+            var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+            if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+            {
+                segmentFormat = "mpegts";
+            }
+
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+                inputModifier,
+                _encodingHelper.GetInputArgument(state, encodingOptions),
+                threads,
+                mapArgs,
+                GetVideoArguments(state, encodingOptions, startNumber),
+                GetAudioArguments(state, encodingOptions),
+                state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+                segmentFormat,
+                startNumberParam,
+                outputTsArg,
+                outputPath).Trim();
+        }
+
+        private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
+        {
+            var audioCodec = _encodingHelper.GetAudioEncoder(state);
+
+            if (!state.IsOutputVideo)
+            {
+                if (EncodingHelper.IsCopyCodec(audioCodec))
+                {
+                    return "-acodec copy";
+                }
+
+                var audioTranscodeParams = new List<string>();
+
+                audioTranscodeParams.Add("-acodec " + audioCodec);
+
+                if (state.OutputAudioBitrate.HasValue)
+                {
+                    audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+                }
+
+                if (state.OutputAudioChannels.HasValue)
+                {
+                    audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+                }
+
+                if (state.OutputAudioSampleRate.HasValue)
+                {
+                    audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+                }
+
+                audioTranscodeParams.Add("-vn");
+                return string.Join(" ", audioTranscodeParams.ToArray());
+            }
+
+            if (EncodingHelper.IsCopyCodec(audioCodec))
+            {
+                var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+                if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
+                {
+                    return "-codec:a:0 copy -copypriorss:a:0 0";
+                }
+
+                return "-codec:a:0 copy";
+            }
+
+            var args = "-codec:a:0 " + audioCodec;
+
+            var channels = state.OutputAudioChannels;
+
+            if (channels.HasValue)
+            {
+                args += " -ac " + channels.Value;
+            }
+
+            var bitrate = state.OutputAudioBitrate;
+
+            if (bitrate.HasValue)
+            {
+                args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+            }
+
+            if (state.OutputAudioSampleRate.HasValue)
+            {
+                args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+            }
+
+            args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
+
+            return args;
+        }
+
+        private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
+        {
+            if (!state.IsOutputVideo)
+            {
+                return string.Empty;
+            }
+
+            var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+
+            var args = "-codec:v:0 " + codec;
+
+            // if (state.EnableMpegtsM2TsMode)
+            // {
+            //     args += " -mpegts_m2ts_mode 1";
+            // }
+
+            // See if we can save come cpu cycles by avoiding encoding
+            if (EncodingHelper.IsCopyCodec(codec))
+            {
+                if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                {
+                    string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+                    if (!string.IsNullOrEmpty(bitStreamArgs))
+                    {
+                        args += " " + bitStreamArgs;
+                    }
+                }
+
+                // args += " -flags -global_header";
+            }
+            else
+            {
+                var gopArg = string.Empty;
+                var keyFrameArg = string.Format(
+                    CultureInfo.InvariantCulture,
+                    " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
+                    startNumber * state.SegmentLength,
+                    state.SegmentLength);
+
+                var framerate = state.VideoStream?.RealFrameRate;
+
+                if (framerate.HasValue)
+                {
+                    // This is to make sure keyframe interval is limited to our segment,
+                    // as forcing keyframes is not enough.
+                    // Example: we encoded half of desired length, then codec detected
+                    // scene cut and inserted a keyframe; next forced keyframe would
+                    // be created outside of segment, which breaks seeking
+                    // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
+                    gopArg = string.Format(
+                        CultureInfo.InvariantCulture,
+                        " -g {0} -keyint_min {0} -sc_threshold 0",
+                        Math.Ceiling(state.SegmentLength * framerate.Value));
+                }
+
+                args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
+
+                // Unable to force key frames using these hw encoders, set key frames by GOP
+                if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
+                {
+                    args += " " + gopArg;
+                }
+                else
+                {
+                    args += " " + keyFrameArg + gopArg;
+                }
+
+                // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
+
+                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+                // This is for graphical subs
+                if (hasGraphicalSubs)
+                {
+                    args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
+                }
+
+                // Add resolution params, if specified
+                else
+                {
+                    args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
+                }
+
+                // -start_at_zero is necessary to use with -ss when seeking,
+                // otherwise the target position cannot be determined.
+                if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
+                {
+                    args += " -start_at_zero";
+                }
+
+                // args += " -flags -global_header";
+            }
+
+            if (!string.IsNullOrEmpty(state.OutputVideoSync))
+            {
+                args += " -vsync " + state.OutputVideoSync;
+            }
+
+            args += _encodingHelper.GetOutputFFlags(state);
+
+            return args;
+        }
+
+        private string GetSegmentFileExtension(string? segmentContainer)
+        {
+            if (!string.IsNullOrWhiteSpace(segmentContainer))
+            {
+                return "." + segmentContainer;
+            }
+
+            return ".ts";
+        }
+
+        private string GetSegmentPath(StreamState state, string playlist, int index)
+        {
+            var folder = Path.GetDirectoryName(playlist);
+
+            var filename = Path.GetFileNameWithoutExtension(playlist);
+
+            return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
+        }
+
+        private async Task<ActionResult> GetSegmentResult(
+            StreamState state,
+            string playlistPath,
+            string segmentPath,
+            string segmentExtension,
+            int segmentIndex,
+            TranscodingJobDto? transcodingJob,
+            CancellationToken cancellationToken)
+        {
+            var segmentExists = System.IO.File.Exists(segmentPath);
+            if (segmentExists)
+            {
+                if (transcodingJob != null && transcodingJob.HasExited)
+                {
+                    // Transcoding job is over, so assume all existing files are ready
+                    _logger.LogDebug("serving up {0} as transcode is over", segmentPath);
+                    return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+                }
+
+                var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+
+                // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
+                if (segmentIndex < currentTranscodingIndex)
+                {
+                    _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
+                    return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+                }
+            }
+
+            var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
+            if (transcodingJob != null)
+            {
+                while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
+                {
+                    // To be considered ready, the segment file has to exist AND
+                    // either the transcoding job should be done or next segment should also exist
+                    if (segmentExists)
+                    {
+                        if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
+                        {
+                            _logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
+                            return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+                        }
+                    }
+                    else
+                    {
+                        segmentExists = System.IO.File.Exists(segmentPath);
+                        if (segmentExists)
+                        {
+                            continue; // avoid unnecessary waiting if segment just became available
+                        }
+                    }
+
+                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                }
+
+                if (!System.IO.File.Exists(segmentPath))
+                {
+                    _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
+                }
+                else
+                {
+                    _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
+                }
+
+                cancellationToken.ThrowIfCancellationRequested();
+            }
+            else
+            {
+                _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+            }
+
+            return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+        }
+
+        private ActionResult GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJobDto? transcodingJob)
+        {
+            var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
+
+            Response.OnCompleted(() =>
+            {
+                _logger.LogDebug("finished serving {0}", segmentPath);
+                if (transcodingJob != null)
+                {
+                    transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
+                    _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+                }
+
+                return Task.CompletedTask;
+            });
+
+            return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this);
+        }
+
+        private long GetEndPositionTicks(StreamState state, int requestedIndex)
+        {
+            double startSeconds = 0;
+            var lengths = GetSegmentLengths(state);
+
+            if (requestedIndex >= lengths.Length)
+            {
+                var msg = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "Invalid segment index requested: {0} - Segment count: {1}",
+                    requestedIndex,
+                    lengths.Length);
+                throw new ArgumentException(msg);
+            }
+
+            for (var i = 0; i <= requestedIndex; i++)
+            {
+                startSeconds += lengths[i];
+            }
+
+            return TimeSpan.FromSeconds(startSeconds).Ticks;
+        }
+
+        private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+        {
+            var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
+
+            if (job == null || job.HasExited)
+            {
+                return null;
+            }
+
+            var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+
+            if (file == null)
+            {
+                return null;
+            }
+
+            var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+
+            var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+
+            return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
+        }
+
+        private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+        {
+            var folder = Path.GetDirectoryName(playlist);
+
+            var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
+
+            try
+            {
+                return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
+                    .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
+                    .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
+                    .FirstOrDefault();
+            }
+            catch (IOException)
+            {
+                return null;
+            }
+        }
+
+        private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+        {
+            var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+
+            if (file != null)
+            {
+                DeleteFile(file.FullName, retryCount);
+            }
+        }
+
+        private void DeleteFile(string path, int retryCount)
+        {
+            if (retryCount >= 5)
+            {
+                return;
+            }
+
+            _logger.LogDebug("Deleting partial HLS file {path}", path);
+
+            try
+            {
+                _fileSystem.DeleteFile(path);
+            }
+            catch (IOException ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
+
+                var task = Task.Delay(100);
+                Task.WaitAll(task);
+                DeleteFile(path, retryCount + 1);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
+            }
+        }
+
+        private long GetStartPositionTicks(StreamState state, int requestedIndex)
+        {
+            double startSeconds = 0;
+            var lengths = GetSegmentLengths(state);
+
+            if (requestedIndex >= lengths.Length)
+            {
+                var msg = string.Format(
+                    CultureInfo.InvariantCulture,
+                    "Invalid segment index requested: {0} - Segment count: {1}",
+                    requestedIndex,
+                    lengths.Length);
+                throw new ArgumentException(msg);
+            }
+
+            for (var i = 0; i < requestedIndex; i++)
+            {
+                startSeconds += lengths[i];
+            }
+
+            var position = TimeSpan.FromSeconds(startSeconds).Ticks;
+            return position;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
new file mode 100644
index 0000000000..95f1906ef0
--- /dev/null
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Globalization;
+using System.Text;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// Hls Codec string helpers.
+    /// </summary>
+    public static class HlsCodecStringHelpers
+    {
+        /// <summary>
+        /// Gets a MP3 codec string.
+        /// </summary>
+        /// <returns>MP3 codec string.</returns>
+        public static string GetMP3String()
+        {
+            return "mp4a.40.34";
+        }
+
+        /// <summary>
+        /// Gets an AAC codec string.
+        /// </summary>
+        /// <param name="profile">AAC profile.</param>
+        /// <returns>AAC codec string.</returns>
+        public static string GetAACString(string profile)
+        {
+            StringBuilder result = new StringBuilder("mp4a", 9);
+
+            if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".40.5");
+            }
+            else
+            {
+                // Default to LC if profile is invalid
+                result.Append(".40.2");
+            }
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets a H.264 codec string.
+        /// </summary>
+        /// <param name="profile">H.264 profile.</param>
+        /// <param name="level">H.264 level.</param>
+        /// <returns>H.264 string.</returns>
+        public static string GetH264String(string profile, int level)
+        {
+            StringBuilder result = new StringBuilder("avc1", 11);
+
+            if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".6400");
+            }
+            else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".4D40");
+            }
+            else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".42E0");
+            }
+            else
+            {
+                // Default to constrained baseline if profile is invalid
+                result.Append(".4240");
+            }
+
+            string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
+            result.Append(levelHex);
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets a H.265 codec string.
+        /// </summary>
+        /// <param name="profile">H.265 profile.</param>
+        /// <param name="level">H.265 level.</param>
+        /// <returns>H.265 string.</returns>
+        public static string GetH265String(string profile, int level)
+        {
+            // The h265 syntax is a bit of a mystery at the time this comment was written.
+            // This is what I've found through various sources:
+            // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
+            StringBuilder result = new StringBuilder("hev1", 16);
+
+            if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".2.6");
+            }
+            else
+            {
+                // Default to main if profile is invalid
+                result.Append(".1.6");
+            }
+
+            result.Append(".L")
+                .Append(level * 3)
+                .Append(".B0");
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets an AC-3 codec string.
+        /// </summary>
+        /// <returns>AC-3 codec string.</returns>
+        public static string GetAC3String()
+        {
+            return "mp4a.a5";
+        }
+
+        /// <summary>
+        /// Gets an E-AC-3 codec string.
+        /// </summary>
+        /// <returns>E-AC-3 codec string.</returns>
+        public static string GetEAC3String()
+        {
+            return "mp4a.a6";
+        }
+    }
+}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 299c7d4aaa..d9e993d496 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -172,5 +173,10 @@ namespace Jellyfin.Api.Helpers
                 .Select(i => i!.Value)
                 .ToArray();
         }
+
+        internal static IPAddress NormalizeIp(IPAddress ip)
+        {
+            return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
+        }
     }
 }
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index fc38eacafd..a5092ac1d2 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -219,7 +219,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="playSessionId">The play session identifier.</param>
         /// <param name="deleteFiles">The delete files.</param>
         /// <returns>Task.</returns>
-        public Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
+        public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
         {
             return KillTranscodingJobs(
                 j => string.IsNullOrWhiteSpace(playSessionId)
@@ -503,9 +503,9 @@ namespace Jellyfin.Api.Helpers
                 }
             }
 
-            var process = new Process()
+            var process = new Process
             {
-                StartInfo = new ProcessStartInfo()
+                StartInfo = new ProcessStartInfo
                 {
                     WindowStyle = ProcessWindowStyle.Hidden,
                     CreateNoWindow = true,
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
new file mode 100644
index 0000000000..3791fadbe6
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+    /// <summary>
+    /// The hls video request dto.
+    /// </summary>
+    public class HlsAudioRequestDto : StreamingRequestDto
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
+        /// </summary>
+        public bool EnableAdaptiveBitrateStreaming { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
new file mode 100644
index 0000000000..7a4be091ba
--- /dev/null
+++ b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Models.StreamingDtos
+{
+    /// <summary>
+    /// The hls video request dto.
+    /// </summary>
+    public class HlsVideoRequestDto : VideoRequestDto
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether enable adaptive bitrate streaming.
+        /// </summary>
+        public bool EnableAdaptiveBitrateStreaming { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index fe5f980b18..a347b365da 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -27,8 +27,6 @@ namespace MediaBrowser.Api.Playback.Hls
     /// <summary>
     /// Options is needed for chromecast. Threw Head in there since it's related
     /// </summary>
-    [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
-    [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
     {
         public bool EnableAdaptiveBitrateStreaming { get; set; }
@@ -39,8 +37,6 @@ namespace MediaBrowser.Api.Playback.Hls
         }
     }
 
-    [Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
-    [Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
     public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
     {
         public bool EnableAdaptiveBitrateStreaming { get; set; }
@@ -56,17 +52,14 @@ namespace MediaBrowser.Api.Playback.Hls
         bool EnableAdaptiveBitrateStreaming { get; set; }
     }
 
-    [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
     public class GetVariantHlsVideoPlaylist : VideoStreamRequest
     {
     }
 
-    [Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
     public class GetVariantHlsAudioPlaylist : StreamRequest
     {
     }
 
-    [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
     public class GetHlsVideoSegment : VideoStreamRequest
     {
         public string PlaylistId { get; set; }
@@ -78,7 +71,6 @@ namespace MediaBrowser.Api.Playback.Hls
         public string SegmentId { get; set; }
     }
 
-    [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
     public class GetHlsAudioSegment : StreamRequest
     {
         public string PlaylistId { get; set; }

From 7c60510bc954c10792a10098f70c6db621a66188 Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Sat, 1 Aug 2020 00:20:47 +0200
Subject: [PATCH 381/463] Experiment for the SSH task.

---
 .ci/azure-pipelines-package.yml | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 10ce8508df..f64dc2ba98 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -132,11 +132,9 @@ jobs:
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     inputs:
       sshEndpoint: repository
-      runOptions: 'inline'
-      inline: |
-        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
-        rm $0
-        exit
+      runOptions: 'commands'
+      commands: bash /srv/repository/collect-server.azure.sh
+      args: /srv/repository/incoming/azure $(Build.BuildNumber) unstable
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
@@ -154,7 +152,7 @@ jobs:
   dependsOn:
   - BuildPackage
   condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
-  
+
   pool:
     vmImage: 'ubuntu-latest'
 

From 3ee28c016aa5e159c9b077a7d16a1e0fae7c2019 Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Sat, 1 Aug 2020 00:32:25 +0200
Subject: [PATCH 382/463] Switch to sudo -n

---
 .ci/azure-pipelines-package.yml | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index f64dc2ba98..ab752d0d93 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -133,7 +133,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: bash /srv/repository/collect-server.azure.sh
+      commands: sudo -n /srv/repository/collect-server.azure.sh
       args: /srv/repository/incoming/azure $(Build.BuildNumber) unstable
 
   - task: SSH@0
@@ -141,11 +141,9 @@ jobs:
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
     inputs:
       sshEndpoint: repository
-      runOptions: 'inline'
-      inline: |
-        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
-        rm $0
-        exit
+      runOptions: 'commands'
+      commands: sudo -n /srv/repository/collect-server.azure.sh
+      args: /srv/repository/incoming/azure $(Build.BuildNumber)
 
 - job: PublishNuget
   displayName: 'Publish NuGet packages'

From a3dcca3826d1b33645d1064da29634cb8a06a3d2 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 14:38:55 +0200
Subject: [PATCH 383/463] Move UniversalAudioService to Jellyfin.Api

---
 Jellyfin.Api/Controllers/AudioController.cs   |   6 +-
 .../Controllers/UniversalAudioController.cs   | 366 ++++++++++
 MediaBrowser.Api/Playback/MediaInfoService.cs | 674 ------------------
 .../Playback/Progressive/AudioService.cs      |  91 ---
 .../Playback/UniversalAudioService.cs         | 401 -----------
 5 files changed, 369 insertions(+), 1169 deletions(-)
 create mode 100644 Jellyfin.Api/Controllers/UniversalAudioController.cs
 delete mode 100644 MediaBrowser.Api/Playback/MediaInfoService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Progressive/AudioService.cs
 delete mode 100644 MediaBrowser.Api/Playback/UniversalAudioService.cs

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index d9afbd9104..ebae1caa0e 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -197,8 +197,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodingReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
-            [FromQuery] Dictionary<string, string> streamOptions)
+            [FromQuery] EncodingContext? context,
+            [FromQuery] Dictionary<string, string>? streamOptions)
         {
             bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
 
@@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodingReasons,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Static,
                 StreamOptions = streamOptions
             };
 
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
new file mode 100644
index 0000000000..9e7b23b78a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -0,0 +1,366 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers
+{
+    /// <summary>
+    /// The universal audio controller.
+    /// </summary>
+    public class UniversalAudioController : BaseJellyfinApiController
+    {
+        private readonly ILoggerFactory _loggerFactory;
+        private readonly IUserManager _userManager;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IDlnaManager _dlnaManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IFileSystem _fileSystem;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IAuthorizationContext _authorizationContext;
+        private readonly INetworkManager _networkManager;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly TranscodingJobHelper _transcodingJobHelper;
+        private readonly IConfiguration _configuration;
+        private readonly ISubtitleEncoder _subtitleEncoder;
+        private readonly IStreamHelper _streamHelper;
+
+        public UniversalAudioController(
+            ILoggerFactory loggerFactory,
+            IServerConfigurationManager serverConfigurationManager,
+            IUserManager userManager,
+            ILibraryManager libraryManager,
+            IMediaEncoder mediaEncoder,
+            IFileSystem fileSystem,
+            IDlnaManager dlnaManager,
+            IDeviceManager deviceManager,
+            IMediaSourceManager mediaSourceManager,
+            IAuthorizationContext authorizationContext,
+            INetworkManager networkManager,
+            TranscodingJobHelper transcodingJobHelper,
+            IConfiguration configuration,
+            ISubtitleEncoder subtitleEncoder,
+            IStreamHelper streamHelper)
+        {
+            _userManager = userManager;
+            _libraryManager = libraryManager;
+            _mediaEncoder = mediaEncoder;
+            _fileSystem = fileSystem;
+            _dlnaManager = dlnaManager;
+            _deviceManager = deviceManager;
+            _mediaSourceManager = mediaSourceManager;
+            _authorizationContext = authorizationContext;
+            _networkManager = networkManager;
+            _loggerFactory = loggerFactory;
+            _serverConfigurationManager = serverConfigurationManager;
+            _transcodingJobHelper = transcodingJobHelper;
+            _configuration = configuration;
+            _subtitleEncoder = subtitleEncoder;
+            _streamHelper = streamHelper;
+        }
+
+        [HttpGet("/Audio/{itemId}/universal")]
+        [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}")]
+        [HttpHead("/Audio/{itemId}/universal")]
+        [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")]
+        public async Task<ActionResult> GetUniversalAudioStream(
+            [FromRoute] Guid itemId,
+            [FromRoute] string? container,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] Guid? userId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] int? transcodingAudioChannels,
+            [FromQuery] long? maxStreamingBitrate,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] string? transcodingContainer,
+            [FromQuery] string? transcodingProtocol,
+            [FromQuery] int? maxAudioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] bool? enableRemoteMedia,
+            [FromQuery] bool breakOnNonKeyFrames,
+            [FromQuery] bool enableRedirection = true)
+        {
+            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+            var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
+            _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
+
+            var mediaInfoController = new MediaInfoController(_mediaSourceManager, _deviceManager, _libraryManager, _networkManager, _mediaEncoder, _userManager, _authorizationContext, _loggerFactory.CreateLogger<MediaInfoController>(), _serverConfigurationManager);
+            var playbackInfoResult = await mediaInfoController.GetPlaybackInfo(itemId, userId).ConfigureAwait(false);
+            var mediaSource = playbackInfoResult.Value.MediaSources[0];
+
+            if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
+            {
+                if (enableRedirection)
+                {
+                    if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
+                    {
+                        return Redirect(mediaSource.Path);
+                    }
+                }
+            }
+
+            var isStatic = mediaSource.SupportsDirectStream;
+            if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+            {
+                // TODO new DynamicHlsController
+                // var dynamicHlsController = new DynamicHlsController();
+                var transcodingProfile = deviceProfile.TranscodingProfiles[0];
+
+                // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+                // TODO: remove this when we switch back to the segment muxer
+                var supportedHLSContainers = new[] { "mpegts", "fmp4" };
+
+                /*
+                var newRequest = new GetMasterHlsAudioPlaylist
+                {
+                    AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
+                    AudioCodec = transcodingProfile.AudioCodec,
+                    Container = ".m3u8",
+                    DeviceId = request.DeviceId,
+                    Id = request.Id,
+                    MaxAudioChannels = request.MaxAudioChannels,
+                    MediaSourceId = mediaSource.Id,
+                    PlaySessionId = playbackInfoResult.PlaySessionId,
+                    StartTimeTicks = request.StartTimeTicks,
+                    Static = isStatic,
+                    // fallback to mpegts if device reports some weird value unsupported by hls
+                    SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
+                    AudioSampleRate = request.MaxAudioSampleRate,
+                    MaxAudioBitDepth = request.MaxAudioBitDepth,
+                    BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
+                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
+                };
+
+                if (isHeadRequest)
+                {
+                    audioController.Request.Method = HttpMethod.Head.Method;
+                    return await service.Head(newRequest).ConfigureAwait(false);
+                }
+
+                return await service.Get(newRequest).ConfigureAwait(false);*/
+                // TODO remove this line
+                return Content(string.Empty);
+            }
+            else
+            {
+                var audioController = new AudioController(
+                    _dlnaManager,
+                    _userManager,
+                    _authorizationContext,
+                    _libraryManager,
+                    _mediaSourceManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _streamHelper,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    // TODO HttpClient
+                    new HttpClient());
+
+                if (isHeadRequest)
+                {
+                    audioController.Request.Method = HttpMethod.Head.Method;
+                    return await audioController.GetAudioStream(
+                            itemId,
+                            isStatic ? null : ("." + mediaSource.TranscodingContainer),
+                            isStatic,
+                            null,
+                            null,
+                            null,
+                            playbackInfoResult.Value.PlaySessionId,
+                            null,
+                            null,
+                            null,
+                            mediaSource.Id,
+                            deviceId,
+                            audioCodec,
+                            null,
+                            null,
+                            null,
+                            breakOnNonKeyFrames,
+                            maxAudioSampleRate,
+                            maxAudioBitDepth,
+                            isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                            null,
+                            maxAudioChannels,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            startTimeTicks,
+                            null,
+                            null,
+                            null,
+                            null,
+                            SubtitleDeliveryMethod.Embed,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                            null,
+                            null,
+                            null,
+                            null)
+                        .ConfigureAwait(false);
+                }
+
+                return await audioController.GetAudioStream(
+                        itemId,
+                        isStatic ? null : ("." + mediaSource.TranscodingContainer),
+                        isStatic,
+                        null,
+                        null,
+                        null,
+                        playbackInfoResult.Value.PlaySessionId,
+                        null,
+                        null,
+                        null,
+                        mediaSource.Id,
+                        deviceId,
+                        audioCodec,
+                        null,
+                        null,
+                        null,
+                        breakOnNonKeyFrames,
+                        maxAudioSampleRate,
+                        maxAudioBitDepth,
+                        isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                        null,
+                        maxAudioChannels,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        startTimeTicks,
+                        null,
+                        null,
+                        null,
+                        null,
+                        SubtitleDeliveryMethod.Embed,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                        null,
+                        null,
+                        null,
+                        null)
+                    .ConfigureAwait(false);
+            }
+        }
+
+        private DeviceProfile GetDeviceProfile(
+            string? container,
+            string? transcodingContainer,
+            string? audioCodec,
+            string? transcodingProtocol,
+            bool? breakOnNonKeyFrames,
+            int? transcodingAudioChannels,
+            int? maxAudioSampleRate,
+            int? maxAudioBitDepth,
+            int? maxAudioChannels)
+        {
+            var deviceProfile = new DeviceProfile();
+
+            var directPlayProfiles = new List<DirectPlayProfile>();
+
+            var containers = (container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+
+            foreach (var cont in containers)
+            {
+                var parts = cont.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+
+                var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
+
+                directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
+            }
+
+            deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
+
+            deviceProfile.TranscodingProfiles = new[]
+            {
+                new TranscodingProfile
+                {
+                    Type = DlnaProfileType.Audio,
+                    Context = EncodingContext.Streaming,
+                    Container = transcodingContainer,
+                    AudioCodec = audioCodec,
+                    Protocol = transcodingProtocol,
+                    BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                    MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
+                }
+            };
+
+            var codecProfiles = new List<CodecProfile>();
+            var conditions = new List<ProfileCondition>();
+
+            if (maxAudioSampleRate.HasValue)
+            {
+                // codec profile
+                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioSampleRate, Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) });
+            }
+
+            if (maxAudioBitDepth.HasValue)
+            {
+                // codec profile
+                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioBitDepth, Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) });
+            }
+
+            if (maxAudioChannels.HasValue)
+            {
+                // codec profile
+                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioChannels, Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) });
+            }
+
+            if (conditions.Count > 0)
+            {
+                // codec profile
+                codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
+            }
+
+            deviceProfile.CodecProfiles = codecProfiles.ToArray();
+
+            return deviceProfile;
+        }
+    }
+}
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
deleted file mode 100644
index 427d255081..0000000000
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ /dev/null
@@ -1,674 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Buffers;
-using System.Globalization;
-using System.Linq;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
-    public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
-    {
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid UserId { get; set; }
-    }
-
-    public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
-    {
-    }
-
-    public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
-    {
-    }
-
-    public class CloseMediaSource : IReturnVoid
-    {
-        [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string LiveStreamId { get; set; }
-    }
-
-    public class GetBitrateTestBytes
-    {
-        [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int Size { get; set; }
-
-        public GetBitrateTestBytes()
-        {
-            // 100k
-            Size = 102400;
-        }
-    }
-
-    [Authenticated]
-    public class MediaInfoService : BaseApiService
-    {
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IDeviceManager _deviceManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly INetworkManager _networkManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IUserManager _userManager;
-        private readonly IAuthorizationContext _authContext;
-
-        public MediaInfoService(
-            ILogger<MediaInfoService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IMediaSourceManager mediaSourceManager,
-            IDeviceManager deviceManager,
-            ILibraryManager libraryManager,
-            INetworkManager networkManager,
-            IMediaEncoder mediaEncoder,
-            IUserManager userManager,
-            IAuthorizationContext authContext)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _mediaSourceManager = mediaSourceManager;
-            _deviceManager = deviceManager;
-            _libraryManager = libraryManager;
-            _networkManager = networkManager;
-            _mediaEncoder = mediaEncoder;
-            _userManager = userManager;
-            _authContext = authContext;
-        }
-
-        public object Get(GetBitrateTestBytes request)
-        {
-            const int MaxSize = 10_000_000;
-
-            var size = request.Size;
-
-            if (size <= 0)
-            {
-                throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request));
-            }
-
-            if (size > MaxSize)
-            {
-                throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request));
-            }
-
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
-            try
-            {
-                new Random().NextBytes(buffer);
-                return ResultFactory.GetResult(null, buffer, "application/octet-stream");
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(buffer);
-            }
-        }
-
-        public async Task<object> Get(GetPlaybackInfo request)
-        {
-            var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
-            return ToOptimizedResult(result);
-        }
-
-        public async Task<object> Post(OpenMediaSource request)
-        {
-            var result = await OpenMediaSource(request).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource request)
-        {
-            var authInfo = _authContext.GetAuthorizationInfo(Request);
-
-            var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
-            var profile = request.DeviceProfile;
-            if (profile == null)
-            {
-                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (caps != null)
-                {
-                    profile = caps.DeviceProfile;
-                }
-            }
-
-            if (profile != null)
-            {
-                var item = _libraryManager.GetItemById(request.ItemId);
-
-                SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
-                    request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
-                    request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, true, true, true);
-            }
-            else
-            {
-                if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
-                {
-                    result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
-                }
-            }
-
-            if (result.MediaSource != null)
-            {
-                NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
-            }
-
-            return result;
-        }
-
-        public void Post(CloseMediaSource request)
-        {
-            _mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult();
-        }
-
-        public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
-        {
-            var authInfo = _authContext.GetAuthorizationInfo(Request);
-
-            var profile = request.DeviceProfile;
-
-            Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
-
-            if (profile == null)
-            {
-                var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (caps != null)
-                {
-                    profile = caps.DeviceProfile;
-                }
-            }
-
-            var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
-
-            if (profile != null)
-            {
-                var mediaSourceId = request.MediaSourceId;
-
-                SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
-            }
-
-            if (request.AutoOpenLiveStream)
-            {
-                var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
-
-                if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
-                {
-                    var openStreamResult = await OpenMediaSource(new OpenMediaSource
-                    {
-                        AudioStreamIndex = request.AudioStreamIndex,
-                        DeviceProfile = request.DeviceProfile,
-                        EnableDirectPlay = request.EnableDirectPlay,
-                        EnableDirectStream = request.EnableDirectStream,
-                        ItemId = request.Id,
-                        MaxAudioChannels = request.MaxAudioChannels,
-                        MaxStreamingBitrate = request.MaxStreamingBitrate,
-                        PlaySessionId = info.PlaySessionId,
-                        StartTimeTicks = request.StartTimeTicks,
-                        SubtitleStreamIndex = request.SubtitleStreamIndex,
-                        UserId = request.UserId,
-                        OpenToken = mediaSource.OpenToken
-                    }).ConfigureAwait(false);
-
-                    info.MediaSources = new[] { openStreamResult.MediaSource };
-                }
-            }
-
-            if (info.MediaSources != null)
-            {
-                foreach (var mediaSource in info.MediaSources)
-                {
-                    NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
-                }
-            }
-
-            return info;
-        }
-
-        private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
-        {
-            mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
-        }
-
-        public async Task<object> Post(GetPostedPlaybackInfo request)
-        {
-            var result = await GetPlaybackInfo(request).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
-        private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
-        {
-            var user = _userManager.GetUserById(userId);
-            var item = _libraryManager.GetItemById(id);
-            var result = new PlaybackInfoResponse();
-
-            MediaSourceInfo[] mediaSources;
-            if (string.IsNullOrWhiteSpace(liveStreamId))
-            {
-
-                // TODO handle supportedLiveMediaTypes?
-                var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
-                if (string.IsNullOrWhiteSpace(mediaSourceId))
-                {
-                    mediaSources = mediaSourcesList.ToArray();
-                }
-                else
-                {
-                    mediaSources = mediaSourcesList
-                        .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
-                        .ToArray();
-                }
-            }
-            else
-            {
-                var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
-
-                mediaSources = new[] { mediaSource };
-            }
-
-            if (mediaSources.Length == 0)
-            {
-                result.MediaSources = Array.Empty<MediaSourceInfo>();
-
-                if (!result.ErrorCode.HasValue)
-                {
-                    result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
-                }
-            }
-            else
-            {
-                // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
-                // Should we move this directly into MediaSourceManager?
-                result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
-
-                result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-            }
-
-            return result;
-        }
-
-        private void SetDeviceSpecificData(
-            Guid itemId,
-            PlaybackInfoResponse result,
-            DeviceProfile profile,
-            AuthorizationInfo auth,
-            long? maxBitrate,
-            long startTimeTicks,
-            string mediaSourceId,
-            int? audioStreamIndex,
-            int? subtitleStreamIndex,
-            int? maxAudioChannels,
-            Guid userId,
-            bool enableDirectPlay,
-            bool forceDirectPlayRemoteMediaSource,
-            bool enableDirectStream,
-            bool enableTranscoding,
-            bool allowVideoStreamCopy,
-            bool allowAudioStreamCopy)
-        {
-            var item = _libraryManager.GetItemById(itemId);
-
-            foreach (var mediaSource in result.MediaSources)
-            {
-                SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
-            }
-
-            SortMediaSources(result, maxBitrate);
-        }
-
-        private void SetDeviceSpecificData(
-            BaseItem item,
-            MediaSourceInfo mediaSource,
-            DeviceProfile profile,
-            AuthorizationInfo auth,
-            long? maxBitrate,
-            long startTimeTicks,
-            string mediaSourceId,
-            int? audioStreamIndex,
-            int? subtitleStreamIndex,
-            int? maxAudioChannels,
-            string playSessionId,
-            Guid userId,
-            bool enableDirectPlay,
-            bool forceDirectPlayRemoteMediaSource,
-            bool enableDirectStream,
-            bool enableTranscoding,
-            bool allowVideoStreamCopy,
-            bool allowAudioStreamCopy)
-        {
-            var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
-
-            var options = new VideoOptions
-            {
-                MediaSources = new[] { mediaSource },
-                Context = EncodingContext.Streaming,
-                DeviceId = auth.DeviceId,
-                ItemId = item.Id,
-                Profile = profile,
-                MaxAudioChannels = maxAudioChannels
-            };
-
-            if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
-            {
-                options.MediaSourceId = mediaSourceId;
-                options.AudioStreamIndex = audioStreamIndex;
-                options.SubtitleStreamIndex = subtitleStreamIndex;
-            }
-
-            var user = _userManager.GetUserById(userId);
-
-            if (!enableDirectPlay)
-            {
-                mediaSource.SupportsDirectPlay = false;
-            }
-
-            if (!enableDirectStream)
-            {
-                mediaSource.SupportsDirectStream = false;
-            }
-
-            if (!enableTranscoding)
-            {
-                mediaSource.SupportsTranscoding = false;
-            }
-
-            if (item is Audio)
-            {
-                Logger.LogInformation(
-                    "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-            else
-            {
-                Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
-                    user.Username,
-                    user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
-                    user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
-                    user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
-            }
-
-            // Beginning of Playback Determination: Attempt DirectPlay first
-            if (mediaSource.SupportsDirectPlay)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectPlay = false;
-                }
-                else
-                {
-                    var supportsDirectStream = mediaSource.SupportsDirectStream;
-
-                    // Dummy this up to fool StreamBuilder
-                    mediaSource.SupportsDirectStream = true;
-                    options.MaxBitrate = maxBitrate;
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectPlay = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectPlay = false;
-                    }
-
-                    // Set this back to what it was
-                    mediaSource.SupportsDirectStream = supportsDirectStream;
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsDirectStream)
-            {
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    mediaSource.SupportsDirectStream = false;
-                }
-                else
-                {
-                    options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                    if (item is Audio)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-                    else if (item is Video)
-                    {
-                        if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
-                            && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
-                        {
-                            options.ForceDirectStream = true;
-                        }
-                    }
-
-                    // The MediaSource supports direct stream, now test to see if the client supports it
-                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                        ? streamBuilder.BuildAudioItem(options)
-                        : streamBuilder.BuildVideoItem(options);
-
-                    if (streamInfo == null || !streamInfo.IsDirectStream)
-                    {
-                        mediaSource.SupportsDirectStream = false;
-                    }
-
-                    if (streamInfo != null)
-                    {
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            if (mediaSource.SupportsTranscoding)
-            {
-                options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
-                // The MediaSource supports direct stream, now test to see if the client supports it
-                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
-                    ? streamBuilder.BuildAudioItem(options)
-                    : streamBuilder.BuildVideoItem(options);
-
-                if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-                        streamInfo.StartPositionTicks = startTimeTicks;
-                        mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-                        mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                        mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-                else
-                {
-                    if (streamInfo != null)
-                    {
-                        streamInfo.PlaySessionId = playSessionId;
-
-                        if (streamInfo.PlayMethod == PlayMethod.Transcode)
-                        {
-                            streamInfo.StartPositionTicks = startTimeTicks;
-                            mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
-                            if (!allowVideoStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
-                            }
-
-                            if (!allowAudioStreamCopy)
-                            {
-                                mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                            }
-
-                            mediaSource.TranscodingContainer = streamInfo.Container;
-                            mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-                        }
-
-                        if (!allowAudioStreamCopy)
-                        {
-                            mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
-                        }
-
-                        mediaSource.TranscodingContainer = streamInfo.Container;
-                        mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
-                        // Do this after the above so that StartPositionTicks is set
-                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
-                    }
-                }
-            }
-
-            foreach (var attachment in mediaSource.MediaAttachments)
-            {
-                attachment.DeliveryUrl = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "/Videos/{0}/{1}/Attachments/{2}",
-                    item.Id,
-                    mediaSource.Id,
-                    attachment.Index);
-            }
-        }
-
-        private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user)
-        {
-            var maxBitrate = clientMaxBitrate;
-            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
-
-            if (remoteClientMaxBitrate <= 0)
-            {
-                remoteClientMaxBitrate = ServerConfigurationManager.Configuration.RemoteClientBitrateLimit;
-            }
-
-            if (remoteClientMaxBitrate > 0)
-            {
-                var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
-
-                Logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
-                if (!isInLocalNetwork)
-                {
-                    maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
-                }
-            }
-
-            return maxBitrate;
-        }
-
-        private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
-        {
-            var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
-            mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
-
-            mediaSource.TranscodeReasons = info.TranscodeReasons;
-
-            foreach (var profile in profiles)
-            {
-                foreach (var stream in mediaSource.MediaStreams)
-                {
-                    if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
-                    {
-                        stream.DeliveryMethod = profile.DeliveryMethod;
-
-                        if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
-                        {
-                            stream.DeliveryUrl = profile.Url.TrimStart('-');
-                            stream.IsExternalUrl = profile.IsExternalUrl;
-                        }
-                    }
-                }
-            }
-        }
-
-        private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
-        {
-            var originalList = result.MediaSources.ToList();
-
-            result.MediaSources = result.MediaSources.OrderBy(i =>
-            {
-                // Nothing beats direct playing a file
-                if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
-                {
-                    return 0;
-                }
-
-                return 1;
-            }).ThenBy(i =>
-            {
-                // Let's assume direct streaming a file is just as desirable as direct playing a remote url
-                if (i.SupportsDirectPlay || i.SupportsDirectStream)
-                {
-                    return 0;
-                }
-
-                return 1;
-            }).ThenBy(i =>
-            {
-                return i.Protocol switch
-                {
-                    MediaProtocol.File => 0,
-                    _ => 1,
-                };
-            }).ThenBy(i =>
-            {
-                if (maxBitrate.HasValue && i.Bitrate.HasValue)
-                {
-                    return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
-                }
-
-                return 1;
-            }).ThenBy(originalList.IndexOf)
-            .ToArray();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
deleted file mode 100644
index 14d402d303..0000000000
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
-    /// <summary>
-    /// Class GetAudioStream.
-    /// </summary>
-    public class GetAudioStream : StreamRequest
-    {
-    }
-
-    /// <summary>
-    /// Class AudioService.
-    /// </summary>
-    // TODO: In order to autheneticate this in the future, Dlna playback will require updating
-    //[Authenticated]
-    public class AudioService : BaseProgressiveStreamingService
-    {
-        public AudioService(
-            ILogger<AudioService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IHttpClient httpClient,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                httpClient,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetAudioStream request)
-        {
-            return ProcessRequest(request, false);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Head(GetAudioStream request)
-        {
-            return ProcessRequest(request, true);
-        }
-
-        protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
-        {
-            return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
deleted file mode 100644
index d5d78cf37a..0000000000
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ /dev/null
@@ -1,401 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Api.Playback.Hls;
-using MediaBrowser.Api.Playback.Progressive;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
-    public class BaseUniversalRequest
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid Id { get; set; }
-
-        [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string MediaSourceId { get; set; }
-
-        [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string DeviceId { get; set; }
-
-        public Guid UserId { get; set; }
-
-        public string AudioCodec { get; set; }
-
-        public string Container { get; set; }
-
-        public int? MaxAudioChannels { get; set; }
-
-        public int? TranscodingAudioChannels { get; set; }
-
-        public long? MaxStreamingBitrate { get; set; }
-
-        [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public long? StartTimeTicks { get; set; }
-
-        public string TranscodingContainer { get; set; }
-
-        public string TranscodingProtocol { get; set; }
-
-        public int? MaxAudioSampleRate { get; set; }
-
-        public int? MaxAudioBitDepth { get; set; }
-
-        public bool EnableRedirection { get; set; }
-
-        public bool EnableRemoteMedia { get; set; }
-
-        public bool BreakOnNonKeyFrames { get; set; }
-
-        public BaseUniversalRequest()
-        {
-            EnableRedirection = true;
-        }
-    }
-
-    [Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
-    [Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
-    public class GetUniversalAudioStream : BaseUniversalRequest
-    {
-    }
-
-    [Authenticated]
-    public class UniversalAudioService : BaseApiService
-    {
-        private readonly EncodingHelper _encodingHelper;
-        private readonly ILoggerFactory _loggerFactory;
-
-        public UniversalAudioService(
-            ILogger<UniversalAudioService> logger,
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IHttpClient httpClient,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            INetworkManager networkManager,
-            EncodingHelper encodingHelper)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            HttpClient = httpClient;
-            UserManager = userManager;
-            LibraryManager = libraryManager;
-            IsoManager = isoManager;
-            MediaEncoder = mediaEncoder;
-            FileSystem = fileSystem;
-            DlnaManager = dlnaManager;
-            DeviceManager = deviceManager;
-            MediaSourceManager = mediaSourceManager;
-            JsonSerializer = jsonSerializer;
-            AuthorizationContext = authorizationContext;
-            NetworkManager = networkManager;
-            _encodingHelper = encodingHelper;
-            _loggerFactory = loggerFactory;
-        }
-
-        protected IHttpClient HttpClient { get; private set; }
-
-        protected IUserManager UserManager { get; private set; }
-
-        protected ILibraryManager LibraryManager { get; private set; }
-
-        protected IIsoManager IsoManager { get; private set; }
-
-        protected IMediaEncoder MediaEncoder { get; private set; }
-
-        protected IFileSystem FileSystem { get; private set; }
-
-        protected IDlnaManager DlnaManager { get; private set; }
-
-        protected IDeviceManager DeviceManager { get; private set; }
-
-        protected IMediaSourceManager MediaSourceManager { get; private set; }
-
-        protected IJsonSerializer JsonSerializer { get; private set; }
-
-        protected IAuthorizationContext AuthorizationContext { get; private set; }
-
-        protected INetworkManager NetworkManager { get; private set; }
-
-        public Task<object> Get(GetUniversalAudioStream request)
-        {
-            return GetUniversalStream(request, false);
-        }
-
-        public Task<object> Head(GetUniversalAudioStream request)
-        {
-            return GetUniversalStream(request, true);
-        }
-
-        private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
-        {
-            var deviceProfile = new DeviceProfile();
-
-            var directPlayProfiles = new List<DirectPlayProfile>();
-
-            var containers = (request.Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-
-            foreach (var container in containers)
-            {
-                var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-
-                var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
-
-                directPlayProfiles.Add(new DirectPlayProfile
-                {
-                    Type = DlnaProfileType.Audio,
-                    Container = parts[0],
-                    AudioCodec = audioCodecs
-                });
-            }
-
-            deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
-
-            deviceProfile.TranscodingProfiles = new[]
-            {
-                new TranscodingProfile
-                {
-                    Type = DlnaProfileType.Audio,
-                    Context = EncodingContext.Streaming,
-                    Container = request.TranscodingContainer,
-                    AudioCodec = request.AudioCodec,
-                    Protocol = request.TranscodingProtocol,
-                    BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
-                    MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
-                }
-            };
-
-            var codecProfiles = new List<CodecProfile>();
-            var conditions = new List<ProfileCondition>();
-
-            if (request.MaxAudioSampleRate.HasValue)
-            {
-                // codec profile
-                conditions.Add(new ProfileCondition
-                {
-                    Condition = ProfileConditionType.LessThanEqual,
-                    IsRequired = false,
-                    Property = ProfileConditionValue.AudioSampleRate,
-                    Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
-                });
-            }
-
-            if (request.MaxAudioBitDepth.HasValue)
-            {
-                // codec profile
-                conditions.Add(new ProfileCondition
-                {
-                    Condition = ProfileConditionType.LessThanEqual,
-                    IsRequired = false,
-                    Property = ProfileConditionValue.AudioBitDepth,
-                    Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
-                });
-            }
-
-            if (request.MaxAudioChannels.HasValue)
-            {
-                // codec profile
-                conditions.Add(new ProfileCondition
-                {
-                    Condition = ProfileConditionType.LessThanEqual,
-                    IsRequired = false,
-                    Property = ProfileConditionValue.AudioChannels,
-                    Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
-                });
-            }
-
-            if (conditions.Count > 0)
-            {
-                // codec profile
-                codecProfiles.Add(new CodecProfile
-                {
-                    Type = CodecType.Audio,
-                    Container = request.Container,
-                    Conditions = conditions.ToArray()
-                });
-            }
-
-            deviceProfile.CodecProfiles = codecProfiles.ToArray();
-
-            return deviceProfile;
-        }
-
-        private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
-        {
-            var deviceProfile = GetDeviceProfile(request);
-
-            AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
-
-            var mediaInfoService = new MediaInfoService(
-                _loggerFactory.CreateLogger<MediaInfoService>(),
-                ServerConfigurationManager,
-                ResultFactory,
-                MediaSourceManager,
-                DeviceManager,
-                LibraryManager,
-                NetworkManager,
-                MediaEncoder,
-                UserManager,
-                AuthorizationContext)
-            {
-                Request = Request
-            };
-
-            var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
-            {
-                Id = request.Id,
-                MaxAudioChannels = request.MaxAudioChannels,
-                MaxStreamingBitrate = request.MaxStreamingBitrate,
-                StartTimeTicks = request.StartTimeTicks,
-                UserId = request.UserId,
-                DeviceProfile = deviceProfile,
-                MediaSourceId = request.MediaSourceId
-            }).ConfigureAwait(false);
-
-            var mediaSource = playbackInfoResult.MediaSources[0];
-
-            if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
-            {
-                if (request.EnableRedirection)
-                {
-                    if (mediaSource.IsRemote && request.EnableRemoteMedia)
-                    {
-                        return ResultFactory.GetRedirectResult(mediaSource.Path);
-                    }
-                }
-            }
-
-            var isStatic = mediaSource.SupportsDirectStream;
-
-            if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
-            {
-                var service = new DynamicHlsService(
-                    _loggerFactory.CreateLogger<DynamicHlsService>(),
-                    ServerConfigurationManager,
-                    ResultFactory,
-                    UserManager,
-                    LibraryManager,
-                    IsoManager,
-                    MediaEncoder,
-                    FileSystem,
-                    DlnaManager,
-                    DeviceManager,
-                    MediaSourceManager,
-                    JsonSerializer,
-                    AuthorizationContext,
-                    NetworkManager,
-                    _encodingHelper)
-                {
-                    Request = Request
-                };
-
-                var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
-                // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
-                // TODO: remove this when we switch back to the segment muxer
-                var supportedHLSContainers = new[] { "mpegts", "fmp4" };
-
-                var newRequest = new GetMasterHlsAudioPlaylist
-                {
-                    AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
-                    AudioCodec = transcodingProfile.AudioCodec,
-                    Container = ".m3u8",
-                    DeviceId = request.DeviceId,
-                    Id = request.Id,
-                    MaxAudioChannels = request.MaxAudioChannels,
-                    MediaSourceId = mediaSource.Id,
-                    PlaySessionId = playbackInfoResult.PlaySessionId,
-                    StartTimeTicks = request.StartTimeTicks,
-                    Static = isStatic,
-                    // fallback to mpegts if device reports some weird value unsupported by hls
-                    SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
-                    AudioSampleRate = request.MaxAudioSampleRate,
-                    MaxAudioBitDepth = request.MaxAudioBitDepth,
-                    BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
-                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
-                };
-
-                if (isHeadRequest)
-                {
-                    return await service.Head(newRequest).ConfigureAwait(false);
-                }
-
-                return await service.Get(newRequest).ConfigureAwait(false);
-            }
-            else
-            {
-                var service = new AudioService(
-                    _loggerFactory.CreateLogger<AudioService>(),
-                    ServerConfigurationManager,
-                    ResultFactory,
-                    HttpClient,
-                    UserManager,
-                    LibraryManager,
-                    IsoManager,
-                    MediaEncoder,
-                    FileSystem,
-                    DlnaManager,
-                    DeviceManager,
-                    MediaSourceManager,
-                    JsonSerializer,
-                    AuthorizationContext,
-                    _encodingHelper)
-                {
-                    Request = Request
-                };
-
-                var newRequest = new GetAudioStream
-                {
-                    AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
-                    AudioCodec = request.AudioCodec,
-                    Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
-                    DeviceId = request.DeviceId,
-                    Id = request.Id,
-                    MaxAudioChannels = request.MaxAudioChannels,
-                    MediaSourceId = mediaSource.Id,
-                    PlaySessionId = playbackInfoResult.PlaySessionId,
-                    StartTimeTicks = request.StartTimeTicks,
-                    Static = isStatic,
-                    AudioSampleRate = request.MaxAudioSampleRate,
-                    MaxAudioBitDepth = request.MaxAudioBitDepth,
-                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
-                };
-
-                if (isHeadRequest)
-                {
-                    return await service.Head(newRequest).ConfigureAwait(false);
-                }
-
-                return await service.Get(newRequest).ConfigureAwait(false);
-            }
-        }
-    }
-}

From d191fec3ac46942b567f4fc2ce9a34ff64302320 Mon Sep 17 00:00:00 2001
From: Bond_009 <Bond.009@outlook.com>
Date: Sat, 1 Aug 2020 15:03:33 +0200
Subject: [PATCH 384/463] Minor fixes for websocket code

---
 .../HttpServer/HttpListenerHost.cs                        | 2 +-
 .../HttpServer/WebSocketConnection.cs                     | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 0d4a789b56..dafdd5b7bf 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
 
                 WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
 
-                var connection = new WebSocketConnection(
+                using var connection = new WebSocketConnection(
                     _loggerFactory.CreateLogger<WebSocketConnection>(),
                     webSocket,
                     context.Connection.RemoteIpAddress,
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 316cd84cfa..d738047e07 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
     /// <summary>
     /// Class WebSocketConnection.
     /// </summary>
-    public class WebSocketConnection : IWebSocketConnection
+    public class WebSocketConnection : IWebSocketConnection, IDisposable
     {
         /// <summary>
         /// The logger.
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
                 Memory<byte> memory = writer.GetMemory(512);
                 try
                 {
-                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
                 }
                 catch (WebSocketException ex)
                 {
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
                 writer.Advance(bytesRead);
 
                 // Make the data available to the PipeReader
-                FlushResult flushResult = await writer.FlushAsync();
+                FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
                 if (flushResult.IsCompleted)
                 {
                     // The PipeReader stopped reading
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
             {
-                await SendKeepAliveResponse();
+                await SendKeepAliveResponse().ConfigureAwait(false);
             }
             else
             {

From d3dc9da5d6780f110bc71b0772d27a97ebb0349f Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 15:09:44 +0200
Subject: [PATCH 385/463] Prepare DynamicHlsController merge

---
 .../Controllers/UniversalAudioController.cs   | 213 +++++++++++++++---
 1 file changed, 176 insertions(+), 37 deletions(-)

diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 9e7b23b78a..311c0a3b9d 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using System.Net.Http;
 using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -15,6 +16,8 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
@@ -40,8 +43,26 @@ namespace Jellyfin.Api.Controllers
         private readonly TranscodingJobHelper _transcodingJobHelper;
         private readonly IConfiguration _configuration;
         private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IStreamHelper _streamHelper;
+        private readonly IHttpClientFactory _httpClientFactory;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
+        /// </summary>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
+        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
         public UniversalAudioController(
             ILoggerFactory loggerFactory,
             IServerConfigurationManager serverConfigurationManager,
@@ -57,7 +78,7 @@ namespace Jellyfin.Api.Controllers
             TranscodingJobHelper transcodingJobHelper,
             IConfiguration configuration,
             ISubtitleEncoder subtitleEncoder,
-            IStreamHelper streamHelper)
+            IHttpClientFactory httpClientFactory)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
@@ -73,13 +94,39 @@ namespace Jellyfin.Api.Controllers
             _transcodingJobHelper = transcodingJobHelper;
             _configuration = configuration;
             _subtitleEncoder = subtitleEncoder;
-            _streamHelper = streamHelper;
+            _httpClientFactory = httpClientFactory;
         }
 
+        /// <summary>
+        /// Gets an audio stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">Optional. The audio container.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="userId">Optional. The user id.</param>
+        /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
+        /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
+        /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
+        /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
+        /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
+        /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
+        /// <response code="200">Audio stream returned.</response>
+        /// <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=universal}.{container?}")]
         [HttpHead("/Audio/{itemId}/universal")]
         [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status302Found)]
         public async Task<ActionResult> GetUniversalAudioStream(
             [FromRoute] Guid itemId,
             [FromRoute] string? container,
@@ -121,44 +168,138 @@ namespace Jellyfin.Api.Controllers
             var isStatic = mediaSource.SupportsDirectStream;
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
-                // TODO new DynamicHlsController
-                // var dynamicHlsController = new DynamicHlsController();
+                var dynamicHlsController = new DynamicHlsController(
+                    _libraryManager,
+                    _userManager,
+                    _dlnaManager,
+                    _authorizationContext,
+                    _mediaSourceManager,
+                    _serverConfigurationManager,
+                    _mediaEncoder,
+                    _fileSystem,
+                    _subtitleEncoder,
+                    _configuration,
+                    _deviceManager,
+                    _transcodingJobHelper,
+                    _networkManager,
+                    _loggerFactory.CreateLogger<DynamicHlsController>());
                 var transcodingProfile = deviceProfile.TranscodingProfiles[0];
 
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
                 // TODO: remove this when we switch back to the segment muxer
-                var supportedHLSContainers = new[] { "mpegts", "fmp4" };
-
-                /*
-                var newRequest = new GetMasterHlsAudioPlaylist
-                {
-                    AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
-                    AudioCodec = transcodingProfile.AudioCodec,
-                    Container = ".m3u8",
-                    DeviceId = request.DeviceId,
-                    Id = request.Id,
-                    MaxAudioChannels = request.MaxAudioChannels,
-                    MediaSourceId = mediaSource.Id,
-                    PlaySessionId = playbackInfoResult.PlaySessionId,
-                    StartTimeTicks = request.StartTimeTicks,
-                    Static = isStatic,
-                    // fallback to mpegts if device reports some weird value unsupported by hls
-                    SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
-                    AudioSampleRate = request.MaxAudioSampleRate,
-                    MaxAudioBitDepth = request.MaxAudioBitDepth,
-                    BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
-                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
-                };
+                var supportedHlsContainers = new[] { "mpegts", "fmp4" };
 
                 if (isHeadRequest)
                 {
-                    audioController.Request.Method = HttpMethod.Head.Method;
-                    return await service.Head(newRequest).ConfigureAwait(false);
+                    dynamicHlsController.Request.Method = HttpMethod.Head.Method;
+                    return await dynamicHlsController.GetMasterHlsAudioPlaylist(
+                        itemId,
+                        ".m3u8",
+                        isStatic,
+                        null,
+                        null,
+                        null,
+                        playbackInfoResult.Value.PlaySessionId,
+                        // fallback to mpegts if device reports some weird value unsupported by hls
+                        Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+                        null,
+                        null,
+                        mediaSource.Id,
+                        deviceId,
+                        transcodingProfile.AudioCodec,
+                        null,
+                        null,
+                        transcodingProfile.BreakOnNonKeyFrames,
+                        maxAudioSampleRate,
+                        maxAudioBitDepth,
+                        null,
+                        isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                        null,
+                        maxAudioChannels,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        startTimeTicks,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null)
+                        .ConfigureAwait(false);
                 }
 
-                return await service.Get(newRequest).ConfigureAwait(false);*/
-                // TODO remove this line
-                return Content(string.Empty);
+                return await dynamicHlsController.GetMasterHlsAudioPlaylist(
+                    itemId,
+                    ".m3u8",
+                    isStatic,
+                    null,
+                    null,
+                    null,
+                    playbackInfoResult.Value.PlaySessionId,
+                    // fallback to mpegts if device reports some weird value unsupported by hls
+                    Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+                    null,
+                    null,
+                    mediaSource.Id,
+                    deviceId,
+                    transcodingProfile.AudioCodec,
+                    null,
+                    null,
+                    transcodingProfile.BreakOnNonKeyFrames,
+                    maxAudioSampleRate,
+                    maxAudioBitDepth,
+                    null,
+                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                    null,
+                    maxAudioChannels,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    startTimeTicks,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null)
+                    .ConfigureAwait(false);
             }
             else
             {
@@ -170,14 +311,12 @@ namespace Jellyfin.Api.Controllers
                     _mediaSourceManager,
                     _serverConfigurationManager,
                     _mediaEncoder,
-                    _streamHelper,
                     _fileSystem,
                     _subtitleEncoder,
                     _configuration,
                     _deviceManager,
                     _transcodingJobHelper,
-                    // TODO HttpClient
-                    new HttpClient());
+                    _httpClientFactory);
 
                 if (isHeadRequest)
                 {
@@ -304,11 +443,11 @@ namespace Jellyfin.Api.Controllers
 
             var directPlayProfiles = new List<DirectPlayProfile>();
 
-            var containers = (container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+            var containers = RequestHelpers.Split(container, ',', true);
 
             foreach (var cont in containers)
             {
-                var parts = cont.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                var parts = RequestHelpers.Split(cont, ',', true);
 
                 var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
 

From afe82087469eee03b434233d5d128b4cd85c336a Mon Sep 17 00:00:00 2001
From: Cody Robibero <cody@robibe.ro>
Date: Sat, 1 Aug 2020 07:52:31 -0600
Subject: [PATCH 386/463] Update
 Jellyfin.Api/Controllers/DynamicHlsController.cs

Co-authored-by: David <daullmer@gmail.com>
---
 Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index c7b84d810d..efe76624ee 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1156,7 +1156,7 @@ namespace Jellyfin.Api.Controllers
 
             var isLiveStream = state.IsSegmentedLiveStream;
 
-            var queryString = Request.Query.ToString();
+            var queryString = Request.QueryString.ToString();
 
             // from universal audio service
             if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))

From 9be20cfaa612bf71c777c7fe4d4c6b7b0e740f11 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 9 Jul 2020 12:44:56 +0200
Subject: [PATCH 387/463] Add jellyfin api entry point

---
 Jellyfin.Api/JellyfinApiEntryPoint.cs         | 405 ++++++++++++++++++
 .../Models/TranscodingDtos/TranscodingJob.cs  | 239 +++++++++++
 2 files changed, 644 insertions(+)
 create mode 100644 Jellyfin.Api/JellyfinApiEntryPoint.cs
 create mode 100644 Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs

diff --git a/Jellyfin.Api/JellyfinApiEntryPoint.cs b/Jellyfin.Api/JellyfinApiEntryPoint.cs
new file mode 100644
index 0000000000..7594abc5d5
--- /dev/null
+++ b/Jellyfin.Api/JellyfinApiEntryPoint.cs
@@ -0,0 +1,405 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.TranscodingDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api
+{
+    /// <summary>
+    /// The jellyfin api entry point.
+    /// </summary>
+    public class JellyfinApiEntryPoint : IServerEntryPoint
+    {
+        private readonly ILogger _logger;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+        private readonly ISessionManager _sessionManager;
+        private readonly IFileSystem _fileSystem;
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly List<TranscodingJob> _activeTranscodingJobs;
+        private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks;
+        private bool _disposed = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="JellyfinApiEntryPoint" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="sessionManager">The session manager.</param>
+        /// <param name="config">The configuration.</param>
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="mediaSourceManager">The media source manager.</param>
+        public JellyfinApiEntryPoint(
+            ILogger<JellyfinApiEntryPoint> logger,
+            ISessionManager sessionManager,
+            IServerConfigurationManager config,
+            IFileSystem fileSystem,
+            IMediaSourceManager mediaSourceManager)
+        {
+            _logger = logger;
+            _sessionManager = sessionManager;
+            _serverConfigurationManager = config;
+            _fileSystem = fileSystem;
+            _mediaSourceManager = mediaSourceManager;
+
+            _activeTranscodingJobs = new List<TranscodingJob>();
+            _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
+
+            _sessionManager!.PlaybackProgress += OnPlaybackProgress;
+            _sessionManager!.PlaybackStart += OnPlaybackProgress;
+
+            Instance = this;
+        }
+
+        /// <summary>
+        /// Gets the initialized instance of <see cref="JellyfinApiEntryPoint"/>.
+        /// </summary>
+        public static JellyfinApiEntryPoint? Instance { get; private set; }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (dispose)
+            {
+                // TODO: dispose
+            }
+
+            List<TranscodingJob> jobs;
+            lock (_activeTranscodingJobs)
+            {
+                jobs = _activeTranscodingJobs.ToList();
+            }
+
+            var jobCount = jobs.Count;
+
+            IEnumerable<Task> GetKillJobs()
+            {
+                foreach (var job in jobs)
+                {
+                    yield return KillTranscodingJob(job, false, path => true);
+                }
+            }
+
+            // Wait for all processes to be killed
+            if (jobCount > 0)
+            {
+                Task.WaitAll(GetKillJobs().ToArray());
+            }
+
+            lock (_activeTranscodingJobs)
+            {
+                _activeTranscodingJobs.Clear();
+            }
+
+            lock (_transcodingLocks)
+            {
+                _transcodingLocks.Clear();
+            }
+
+            _sessionManager.PlaybackProgress -= OnPlaybackProgress;
+            _sessionManager.PlaybackStart -= OnPlaybackProgress;
+
+            _disposed = true;
+        }
+
+        /// <inheritdoc />
+        public Task RunAsync()
+        {
+            try
+            {
+                DeleteEncodedMediaCache();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error deleting encoded media cache");
+            }
+
+            return Task.CompletedTask;
+        }
+
+        private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        {
+            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+            {
+                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the encoded media cache.
+        /// </summary>
+        private void DeleteEncodedMediaCache()
+        {
+            var path = _serverConfigurationManager.GetTranscodePath();
+            if (!Directory.Exists(path))
+            {
+                return;
+            }
+
+            foreach (var file in _fileSystem.GetFilePaths(path, true))
+            {
+                _fileSystem.DeleteFile(file);
+            }
+        }
+
+        internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
+        {
+            if (string.IsNullOrEmpty(playSessionId))
+            {
+                throw new ArgumentNullException(nameof(playSessionId));
+            }
+
+            _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
+
+            List<TranscodingJob> jobs;
+
+            lock (_activeTranscodingJobs)
+            {
+                // This is really only needed for HLS.
+                // Progressive streams can stop on their own reliably
+                jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
+            }
+
+            foreach (var job in jobs)
+            {
+                if (isUserPaused.HasValue)
+                {
+                    _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
+                    job.IsUserPaused = isUserPaused.Value;
+                }
+
+                PingTimer(job, true);
+            }
+        }
+
+        private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
+        {
+            if (job.HasExited)
+            {
+                job.StopKillTimer();
+                return;
+            }
+
+            var timerDuration = 10000;
+
+            if (job.Type != TranscodingJobType.Progressive)
+            {
+                timerDuration = 60000;
+            }
+
+            job.PingTimeout = timerDuration;
+            job.LastPingDate = DateTime.UtcNow;
+
+            // Don't start the timer for playback checkins with progressive streaming
+            if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
+            {
+                job.StartKillTimer(OnTranscodeKillTimerStopped);
+            }
+            else
+            {
+                job.ChangeKillTimerIfStarted();
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode kill timer stopped].
+        /// </summary>
+        /// <param name="state">The state.</param>
+        private async void OnTranscodeKillTimerStopped(object state)
+        {
+            var job = (TranscodingJob)state;
+
+            if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
+            {
+                var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
+
+                if (timeSinceLastPing < job.PingTimeout)
+                {
+                    job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+                    return;
+                }
+            }
+
+            _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+            await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Kills the transcoding job.
+        /// </summary>
+        /// <param name="job">The job.</param>
+        /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
+        /// <param name="delete">The delete.</param>
+        private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
+        {
+            job.DisposeKillTimer();
+
+            _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+
+            lock (_activeTranscodingJobs)
+            {
+                _activeTranscodingJobs.Remove(job);
+
+                if (!job.CancellationTokenSource!.IsCancellationRequested)
+                {
+                    job.CancellationTokenSource.Cancel();
+                }
+            }
+
+            lock (_transcodingLocks)
+            {
+                _transcodingLocks.Remove(job.Path!);
+            }
+
+            lock (job)
+            {
+                job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+
+                var process = job.Process;
+
+                var hasExited = job.HasExited;
+
+                if (!hasExited)
+                {
+                    try
+                    {
+                        _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
+
+                        process?.StandardInput.WriteLine("q");
+
+                        // Need to wait (an arbitrary amount of time) because killing is asynchronous
+                        if (!process!.WaitForExit(5000))
+                        {
+                            _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
+                            process.Kill();
+                        }
+                    }
+                    catch (InvalidOperationException)
+                    {
+                    }
+                }
+            }
+
+            if (delete(job.Path!))
+            {
+                await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
+            }
+
+            if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+            {
+                try
+                {
+                    await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
+                }
+            }
+        }
+
+        private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
+        {
+            if (retryCount >= 10)
+            {
+                return;
+            }
+
+            _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
+
+            await Task.Delay(delayMs).ConfigureAwait(false);
+
+            try
+            {
+                if (jobType == TranscodingJobType.Progressive)
+                {
+                    DeleteProgressivePartialStreamFiles(path);
+                }
+                else
+                {
+                    DeleteHlsPartialStreamFiles(path);
+                }
+            }
+            catch (IOException ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+
+                await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the progressive partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        private void DeleteProgressivePartialStreamFiles(string outputFilePath)
+        {
+            if (File.Exists(outputFilePath))
+            {
+                _fileSystem.DeleteFile(outputFilePath);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the HLS partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        private void DeleteHlsPartialStreamFiles(string outputFilePath)
+        {
+            var directory = Path.GetDirectoryName(outputFilePath);
+            var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+            var filesToDelete = _fileSystem.GetFilePaths(directory)
+                .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+
+            List<Exception>? exs = null;
+            foreach (var file in filesToDelete)
+            {
+                try
+                {
+                    _logger.LogDebug("Deleting HLS file {0}", file);
+                    _fileSystem.DeleteFile(file);
+                }
+                catch (IOException ex)
+                {
+                    (exs ??= new List<Exception>(4)).Add(ex);
+                    _logger.LogError(ex, "Error deleting HLS file {Path}", file);
+                }
+            }
+
+            if (exs != null)
+            {
+                throw new AggregateException("Error deleting HLS files", exs);
+            }
+        }
+    }
+}
diff --git a/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs b/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs
new file mode 100644
index 0000000000..9b72107cfc
--- /dev/null
+++ b/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Models.TranscodingDtos
+{
+    /// <summary>
+    /// The transcoding job.
+    /// </summary>
+    public class TranscodingJob
+    {
+        private readonly ILogger _logger;
+        private readonly object _timerLock = new object();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TranscodingJob"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJob}"/> interface.</param>
+        public TranscodingJob(ILogger<TranscodingJob> logger)
+        {
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Gets or sets the play session identifier.
+        /// </summary>
+        /// <value>The play session identifier.</value>
+        public string? PlaySessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the live stream identifier.
+        /// </summary>
+        /// <value>The live stream identifier.</value>
+        public string? LiveStreamId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the transcoding job is a live output.
+        /// </summary>
+        public bool IsLiveOutput { get; set; }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public MediaSourceInfo? MediaSource { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transcoding path.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public TranscodingJobType Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the process.
+        /// </summary>
+        /// <value>The process.</value>
+        public Process? Process { get; set; }
+
+        /// <summary>
+        /// Gets or sets the active request count.
+        /// </summary>
+        /// <value>The active request count.</value>
+        public int ActiveRequestCount { get; set; }
+
+        /// <summary>
+        /// Gets or sets the kill timer.
+        /// </summary>
+        /// <value>The kill timer.</value>
+        private Timer? KillTimer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device id.
+        /// </summary>
+        public string? DeviceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the cancellation token source.
+        /// </summary>
+        public CancellationTokenSource? CancellationTokenSource { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the transcoding job has exited.
+        /// </summary>
+        public bool HasExited { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user has paused the video.
+        /// </summary>
+        public bool IsUserPaused { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        public string? Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the framerate.
+        /// </summary>
+        public float? Framerate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the completion percentage.
+        /// </summary>
+        public double? CompletionPercentage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the bytes downloaded.
+        /// </summary>
+        public long? BytesDownloaded { get; set; }
+
+        /// <summary>
+        /// Gets or sets the bytes transcoded.
+        /// </summary>
+        public long? BytesTranscoded { get; set; }
+
+        /// <summary>
+        /// Gets or sets the bitrate.
+        /// </summary>
+        public int? BitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transcoding position ticks.
+        /// </summary>
+        public long? TranscodingPositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the download position ticks.
+        /// </summary>
+        public long? DownloadPositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transcodign throttler.
+        /// </summary>
+        public TranscodingThrottler? TranscodingThrottler { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last ping datetime.
+        /// </summary>
+        public DateTime LastPingDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ping timeout.
+        /// </summary>
+        public int PingTimeout { get; set; }
+
+        /// <summary>
+        /// Stops the kill timer.
+        /// </summary>
+        public void StopKillTimer()
+        {
+            lock (_timerLock)
+            {
+                KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+            }
+        }
+
+        /// <summary>
+        /// Disposes the kill timer.
+        /// </summary>
+        public void DisposeKillTimer()
+        {
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    KillTimer.Dispose();
+                    KillTimer = null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Starts the kill timer.
+        /// </summary>
+        /// <param name="callback">The amount of ms the timer should wait before the transcoding job gets killed.</param>
+        public void StartKillTimer(Action<object> callback)
+        {
+            StartKillTimer(callback, PingTimeout);
+        }
+
+        /// <summary>
+        /// Starts the kill timer.
+        /// </summary>
+        /// <param name="callback">The <see cref="Action"/> to run when the kill timer has finished.</param>
+        /// <param name="intervalMs">The amount of ms the timer should wait before the transcoding job gets killed.</param>
+        public void StartKillTimer(Action<object> callback, int intervalMs)
+        {
+            if (HasExited)
+            {
+                return;
+            }
+
+            lock (_timerLock)
+            {
+                if (KillTimer == null)
+                {
+                    _logger.LogDebug($"Starting kill timer at {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
+                    KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
+                }
+                else
+                {
+                    _logger.LogDebug($"Changing kill timer to {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Changes the kill timer if it has started.
+        /// </summary>
+        public void ChangeKillTimerIfStarted()
+        {
+            if (HasExited)
+            {
+                return;
+            }
+
+            lock (_timerLock)
+            {
+                if (KillTimer != null)
+                {
+                    var intervalMs = PingTimeout;
+
+                    _logger.LogDebug($"Changing kill timer to {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
+                    KillTimer.Change(intervalMs, Timeout.Infinite);
+                }
+            }
+        }
+    }
+}

From b717ecd5e0986143099cafeb1c3e154b72d04512 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Thu, 9 Jul 2020 17:22:30 +0200
Subject: [PATCH 388/463] Move methods to the right class

---
 .../Controllers/PlaystateController.cs        |   2 +-
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs  |  13 +-
 Jellyfin.Api/JellyfinApiEntryPoint.cs         | 405 ------------------
 .../Models/TranscodingDtos/TranscodingJob.cs  | 239 -----------
 4 files changed, 13 insertions(+), 646 deletions(-)
 delete mode 100644 Jellyfin.Api/JellyfinApiEntryPoint.cs
 delete mode 100644 Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs

diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 3ebd003f1a..e893f4f4cb 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -55,7 +56,6 @@ namespace Jellyfin.Api.Controllers
             _sessionManager = sessionManager;
             _authContext = authContext;
             _logger = loggerFactory.CreateLogger<PlaystateController>();
-
             _transcodingJobHelper = transcodingJobHelper;
         }
 
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index fc38eacafd..13d8c26db6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -53,6 +53,7 @@ namespace Jellyfin.Api.Helpers
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly ISessionManager _sessionManager;
         private readonly ILoggerFactory _loggerFactory;
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
@@ -90,10 +91,12 @@ namespace Jellyfin.Api.Helpers
             _authorizationContext = authorizationContext;
             _isoManager = isoManager;
             _loggerFactory = loggerFactory;
-
             _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
 
             DeleteEncodedMediaCache();
+
+            sessionManager!.PlaybackProgress += OnPlaybackProgress;
+            sessionManager!.PlaybackStart += OnPlaybackProgress;
         }
 
         /// <summary>
@@ -834,6 +837,14 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        {
+            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+            {
+                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+            }
+        }
+
         /// <summary>
         /// Deletes the encoded media cache.
         /// </summary>
diff --git a/Jellyfin.Api/JellyfinApiEntryPoint.cs b/Jellyfin.Api/JellyfinApiEntryPoint.cs
deleted file mode 100644
index 7594abc5d5..0000000000
--- a/Jellyfin.Api/JellyfinApiEntryPoint.cs
+++ /dev/null
@@ -1,405 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Api.Models.TranscodingDtos;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Api
-{
-    /// <summary>
-    /// The jellyfin api entry point.
-    /// </summary>
-    public class JellyfinApiEntryPoint : IServerEntryPoint
-    {
-        private readonly ILogger _logger;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly ISessionManager _sessionManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly List<TranscodingJob> _activeTranscodingJobs;
-        private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks;
-        private bool _disposed = false;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JellyfinApiEntryPoint" /> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        /// <param name="config">The configuration.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="mediaSourceManager">The media source manager.</param>
-        public JellyfinApiEntryPoint(
-            ILogger<JellyfinApiEntryPoint> logger,
-            ISessionManager sessionManager,
-            IServerConfigurationManager config,
-            IFileSystem fileSystem,
-            IMediaSourceManager mediaSourceManager)
-        {
-            _logger = logger;
-            _sessionManager = sessionManager;
-            _serverConfigurationManager = config;
-            _fileSystem = fileSystem;
-            _mediaSourceManager = mediaSourceManager;
-
-            _activeTranscodingJobs = new List<TranscodingJob>();
-            _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
-
-            _sessionManager!.PlaybackProgress += OnPlaybackProgress;
-            _sessionManager!.PlaybackStart += OnPlaybackProgress;
-
-            Instance = this;
-        }
-
-        /// <summary>
-        /// Gets the initialized instance of <see cref="JellyfinApiEntryPoint"/>.
-        /// </summary>
-        public static JellyfinApiEntryPoint? Instance { get; private set; }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (dispose)
-            {
-                // TODO: dispose
-            }
-
-            List<TranscodingJob> jobs;
-            lock (_activeTranscodingJobs)
-            {
-                jobs = _activeTranscodingJobs.ToList();
-            }
-
-            var jobCount = jobs.Count;
-
-            IEnumerable<Task> GetKillJobs()
-            {
-                foreach (var job in jobs)
-                {
-                    yield return KillTranscodingJob(job, false, path => true);
-                }
-            }
-
-            // Wait for all processes to be killed
-            if (jobCount > 0)
-            {
-                Task.WaitAll(GetKillJobs().ToArray());
-            }
-
-            lock (_activeTranscodingJobs)
-            {
-                _activeTranscodingJobs.Clear();
-            }
-
-            lock (_transcodingLocks)
-            {
-                _transcodingLocks.Clear();
-            }
-
-            _sessionManager.PlaybackProgress -= OnPlaybackProgress;
-            _sessionManager.PlaybackStart -= OnPlaybackProgress;
-
-            _disposed = true;
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            try
-            {
-                DeleteEncodedMediaCache();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error deleting encoded media cache");
-            }
-
-            return Task.CompletedTask;
-        }
-
-        private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
-        {
-            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
-            {
-                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the encoded media cache.
-        /// </summary>
-        private void DeleteEncodedMediaCache()
-        {
-            var path = _serverConfigurationManager.GetTranscodePath();
-            if (!Directory.Exists(path))
-            {
-                return;
-            }
-
-            foreach (var file in _fileSystem.GetFilePaths(path, true))
-            {
-                _fileSystem.DeleteFile(file);
-            }
-        }
-
-        internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
-        {
-            if (string.IsNullOrEmpty(playSessionId))
-            {
-                throw new ArgumentNullException(nameof(playSessionId));
-            }
-
-            _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
-
-            List<TranscodingJob> jobs;
-
-            lock (_activeTranscodingJobs)
-            {
-                // This is really only needed for HLS.
-                // Progressive streams can stop on their own reliably
-                jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-
-            foreach (var job in jobs)
-            {
-                if (isUserPaused.HasValue)
-                {
-                    _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
-                    job.IsUserPaused = isUserPaused.Value;
-                }
-
-                PingTimer(job, true);
-            }
-        }
-
-        private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
-        {
-            if (job.HasExited)
-            {
-                job.StopKillTimer();
-                return;
-            }
-
-            var timerDuration = 10000;
-
-            if (job.Type != TranscodingJobType.Progressive)
-            {
-                timerDuration = 60000;
-            }
-
-            job.PingTimeout = timerDuration;
-            job.LastPingDate = DateTime.UtcNow;
-
-            // Don't start the timer for playback checkins with progressive streaming
-            if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
-            {
-                job.StartKillTimer(OnTranscodeKillTimerStopped);
-            }
-            else
-            {
-                job.ChangeKillTimerIfStarted();
-            }
-        }
-
-        /// <summary>
-        /// Called when [transcode kill timer stopped].
-        /// </summary>
-        /// <param name="state">The state.</param>
-        private async void OnTranscodeKillTimerStopped(object state)
-        {
-            var job = (TranscodingJob)state;
-
-            if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
-            {
-                var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
-
-                if (timeSinceLastPing < job.PingTimeout)
-                {
-                    job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
-                    return;
-                }
-            }
-
-            _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
-            await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Kills the transcoding job.
-        /// </summary>
-        /// <param name="job">The job.</param>
-        /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
-        /// <param name="delete">The delete.</param>
-        private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
-        {
-            job.DisposeKillTimer();
-
-            _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
-            lock (_activeTranscodingJobs)
-            {
-                _activeTranscodingJobs.Remove(job);
-
-                if (!job.CancellationTokenSource!.IsCancellationRequested)
-                {
-                    job.CancellationTokenSource.Cancel();
-                }
-            }
-
-            lock (_transcodingLocks)
-            {
-                _transcodingLocks.Remove(job.Path!);
-            }
-
-            lock (job)
-            {
-                job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
-
-                var process = job.Process;
-
-                var hasExited = job.HasExited;
-
-                if (!hasExited)
-                {
-                    try
-                    {
-                        _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
-
-                        process?.StandardInput.WriteLine("q");
-
-                        // Need to wait (an arbitrary amount of time) because killing is asynchronous
-                        if (!process!.WaitForExit(5000))
-                        {
-                            _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
-                            process.Kill();
-                        }
-                    }
-                    catch (InvalidOperationException)
-                    {
-                    }
-                }
-            }
-
-            if (delete(job.Path!))
-            {
-                await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
-            }
-
-            if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
-            {
-                try
-                {
-                    await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
-                }
-            }
-        }
-
-        private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
-        {
-            if (retryCount >= 10)
-            {
-                return;
-            }
-
-            _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
-
-            await Task.Delay(delayMs).ConfigureAwait(false);
-
-            try
-            {
-                if (jobType == TranscodingJobType.Progressive)
-                {
-                    DeleteProgressivePartialStreamFiles(path);
-                }
-                else
-                {
-                    DeleteHlsPartialStreamFiles(path);
-                }
-            }
-            catch (IOException ex)
-            {
-                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
-
-                await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the progressive partial stream files.
-        /// </summary>
-        /// <param name="outputFilePath">The output file path.</param>
-        private void DeleteProgressivePartialStreamFiles(string outputFilePath)
-        {
-            if (File.Exists(outputFilePath))
-            {
-                _fileSystem.DeleteFile(outputFilePath);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the HLS partial stream files.
-        /// </summary>
-        /// <param name="outputFilePath">The output file path.</param>
-        private void DeleteHlsPartialStreamFiles(string outputFilePath)
-        {
-            var directory = Path.GetDirectoryName(outputFilePath);
-            var name = Path.GetFileNameWithoutExtension(outputFilePath);
-
-            var filesToDelete = _fileSystem.GetFilePaths(directory)
-                .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
-
-            List<Exception>? exs = null;
-            foreach (var file in filesToDelete)
-            {
-                try
-                {
-                    _logger.LogDebug("Deleting HLS file {0}", file);
-                    _fileSystem.DeleteFile(file);
-                }
-                catch (IOException ex)
-                {
-                    (exs ??= new List<Exception>(4)).Add(ex);
-                    _logger.LogError(ex, "Error deleting HLS file {Path}", file);
-                }
-            }
-
-            if (exs != null)
-            {
-                throw new AggregateException("Error deleting HLS files", exs);
-            }
-        }
-    }
-}
diff --git a/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs b/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs
deleted file mode 100644
index 9b72107cfc..0000000000
--- a/Jellyfin.Api/Models/TranscodingDtos/TranscodingJob.cs
+++ /dev/null
@@ -1,239 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-using Jellyfin.Api.Models.PlaybackDtos;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dto;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Api.Models.TranscodingDtos
-{
-    /// <summary>
-    /// The transcoding job.
-    /// </summary>
-    public class TranscodingJob
-    {
-        private readonly ILogger _logger;
-        private readonly object _timerLock = new object();
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="TranscodingJob"/> class.
-        /// </summary>
-        /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJob}"/> interface.</param>
-        public TranscodingJob(ILogger<TranscodingJob> logger)
-        {
-            _logger = logger;
-        }
-
-        /// <summary>
-        /// Gets or sets the play session identifier.
-        /// </summary>
-        /// <value>The play session identifier.</value>
-        public string? PlaySessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the live stream identifier.
-        /// </summary>
-        /// <value>The live stream identifier.</value>
-        public string? LiveStreamId { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the transcoding job is a live output.
-        /// </summary>
-        public bool IsLiveOutput { get; set; }
-
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public MediaSourceInfo? MediaSource { get; set; }
-
-        /// <summary>
-        /// Gets or sets the transcoding path.
-        /// </summary>
-        public string? Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type.
-        /// </summary>
-        /// <value>The type.</value>
-        public TranscodingJobType Type { get; set; }
-
-        /// <summary>
-        /// Gets or sets the process.
-        /// </summary>
-        /// <value>The process.</value>
-        public Process? Process { get; set; }
-
-        /// <summary>
-        /// Gets or sets the active request count.
-        /// </summary>
-        /// <value>The active request count.</value>
-        public int ActiveRequestCount { get; set; }
-
-        /// <summary>
-        /// Gets or sets the kill timer.
-        /// </summary>
-        /// <value>The kill timer.</value>
-        private Timer? KillTimer { get; set; }
-
-        /// <summary>
-        /// Gets or sets the device id.
-        /// </summary>
-        public string? DeviceId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the cancellation token source.
-        /// </summary>
-        public CancellationTokenSource? CancellationTokenSource { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the transcoding job has exited.
-        /// </summary>
-        public bool HasExited { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the user has paused the video.
-        /// </summary>
-        public bool IsUserPaused { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        public string? Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the framerate.
-        /// </summary>
-        public float? Framerate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the completion percentage.
-        /// </summary>
-        public double? CompletionPercentage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the bytes downloaded.
-        /// </summary>
-        public long? BytesDownloaded { get; set; }
-
-        /// <summary>
-        /// Gets or sets the bytes transcoded.
-        /// </summary>
-        public long? BytesTranscoded { get; set; }
-
-        /// <summary>
-        /// Gets or sets the bitrate.
-        /// </summary>
-        public int? BitRate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the transcoding position ticks.
-        /// </summary>
-        public long? TranscodingPositionTicks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the download position ticks.
-        /// </summary>
-        public long? DownloadPositionTicks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the transcodign throttler.
-        /// </summary>
-        public TranscodingThrottler? TranscodingThrottler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the last ping datetime.
-        /// </summary>
-        public DateTime LastPingDate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the ping timeout.
-        /// </summary>
-        public int PingTimeout { get; set; }
-
-        /// <summary>
-        /// Stops the kill timer.
-        /// </summary>
-        public void StopKillTimer()
-        {
-            lock (_timerLock)
-            {
-                KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
-            }
-        }
-
-        /// <summary>
-        /// Disposes the kill timer.
-        /// </summary>
-        public void DisposeKillTimer()
-        {
-            lock (_timerLock)
-            {
-                if (KillTimer != null)
-                {
-                    KillTimer.Dispose();
-                    KillTimer = null;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Starts the kill timer.
-        /// </summary>
-        /// <param name="callback">The amount of ms the timer should wait before the transcoding job gets killed.</param>
-        public void StartKillTimer(Action<object> callback)
-        {
-            StartKillTimer(callback, PingTimeout);
-        }
-
-        /// <summary>
-        /// Starts the kill timer.
-        /// </summary>
-        /// <param name="callback">The <see cref="Action"/> to run when the kill timer has finished.</param>
-        /// <param name="intervalMs">The amount of ms the timer should wait before the transcoding job gets killed.</param>
-        public void StartKillTimer(Action<object> callback, int intervalMs)
-        {
-            if (HasExited)
-            {
-                return;
-            }
-
-            lock (_timerLock)
-            {
-                if (KillTimer == null)
-                {
-                    _logger.LogDebug($"Starting kill timer at {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
-                    KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
-                }
-                else
-                {
-                    _logger.LogDebug($"Changing kill timer to {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
-                    KillTimer.Change(intervalMs, Timeout.Infinite);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Changes the kill timer if it has started.
-        /// </summary>
-        public void ChangeKillTimerIfStarted()
-        {
-            if (HasExited)
-            {
-                return;
-            }
-
-            lock (_timerLock)
-            {
-                if (KillTimer != null)
-                {
-                    var intervalMs = PingTimeout;
-
-                    _logger.LogDebug($"Changing kill timer to {intervalMs}ms. JobId {Id} PlaySessionId {PlaySessionId}");
-                    KillTimer.Change(intervalMs, Timeout.Infinite);
-                }
-            }
-        }
-    }
-}

From 3f0c0e2d0d920871e74ada6f3a1f1e17d8ce0c28 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 16:21:48 +0200
Subject: [PATCH 389/463] Implement IDisposable

---
 Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 27 +++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 13d8c26db6..38bd7efeef 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Helpers
     /// <summary>
     /// Transcoding job helpers.
     /// </summary>
-    public class TranscodingJobHelper
+    public class TranscodingJobHelper : IDisposable
     {
         /// <summary>
         /// The active transcoding jobs.
@@ -46,14 +46,12 @@ namespace Jellyfin.Api.Helpers
         private readonly EncodingHelper _encodingHelper;
         private readonly IFileSystem _fileSystem;
         private readonly IIsoManager _isoManager;
-
         private readonly ILogger<TranscodingJobHelper> _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly ISessionManager _sessionManager;
         private readonly ILoggerFactory _loggerFactory;
-        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
@@ -861,5 +859,28 @@ namespace Jellyfin.Api.Helpers
                 _fileSystem.DeleteFile(file);
             }
         }
+
+        /// <summary>
+        /// Dispose transcoding job helper.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Dispose throttler.
+        /// </summary>
+        /// <param name="disposing">Disposing.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _loggerFactory.Dispose();
+                _sessionManager!.PlaybackProgress -= OnPlaybackProgress;
+                _sessionManager!.PlaybackStart -= OnPlaybackProgress;
+            }
+        }
     }
 }

From 52ba54a71b8290d45ed0cbf9f3673b66d99c62e1 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sat, 1 Aug 2020 08:58:18 -0600
Subject: [PATCH 390/463] PERFORMANCE

---
 Jellyfin.Api/Controllers/DynamicHlsController.cs | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index efe76624ee..b7e1837c97 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1145,7 +1145,7 @@ namespace Jellyfin.Api.Controllers
             Response.Headers.Add(HeaderNames.Expires, "0");
             if (isHeadRequest)
             {
-                return new FileContentResult(Encoding.UTF8.GetBytes(string.Empty), MimeTypes.GetMimeType("playlist.m3u8"));
+                return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
             }
 
             var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
@@ -1413,11 +1413,10 @@ namespace Jellyfin.Api.Controllers
         private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
         {
             var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
+            const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
 
             foreach (var stream in subtitles)
             {
-                const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
                 var name = stream.DisplayTitle;
 
                 var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
@@ -1433,7 +1432,7 @@ namespace Jellyfin.Api.Controllers
 
                 var line = string.Format(
                     CultureInfo.InvariantCulture,
-                    format,
+                    Format,
                     name,
                     isDefault ? "YES" : "NO",
                     isForced ? "YES" : "NO",

From 91f2a7d9a831a63d4e833272037e7f171b729c32 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 18:04:46 +0200
Subject: [PATCH 391/463] Revert changes

---
 Jellyfin.Api/Controllers/PlaystateController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index e893f4f4cb..3ebd003f1a 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -4,7 +4,6 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -56,6 +55,7 @@ namespace Jellyfin.Api.Controllers
             _sessionManager = sessionManager;
             _authContext = authContext;
             _logger = loggerFactory.CreateLogger<PlaystateController>();
+
             _transcodingJobHelper = transcodingJobHelper;
         }
 

From 24c5016f487193791a807c14b12b388939320240 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 18:22:40 +0200
Subject: [PATCH 392/463] Fix parameters

---
 .../Controllers/UniversalAudioController.cs    | 18 ++++++++----------
 1 file changed, 8 insertions(+), 10 deletions(-)

diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 311c0a3b9d..0c7ef258a7 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -209,12 +209,12 @@ namespace Jellyfin.Api.Controllers
                         transcodingProfile.AudioCodec,
                         null,
                         null,
+                        null,
                         transcodingProfile.BreakOnNonKeyFrames,
                         maxAudioSampleRate,
                         maxAudioBitDepth,
                         null,
                         isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                        null,
                         maxAudioChannels,
                         null,
                         null,
@@ -226,6 +226,7 @@ namespace Jellyfin.Api.Controllers
                         null,
                         null,
                         null,
+                        SubtitleDeliveryMethod.Hls,
                         null,
                         null,
                         null,
@@ -240,10 +241,8 @@ namespace Jellyfin.Api.Controllers
                         mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
                         null,
                         null,
-                        null,
-                        null,
-                        null,
-                        null)
+                        EncodingContext.Static,
+                        new Dictionary<string, string>())
                         .ConfigureAwait(false);
                 }
 
@@ -264,12 +263,12 @@ namespace Jellyfin.Api.Controllers
                     transcodingProfile.AudioCodec,
                     null,
                     null,
+                    null,
                     transcodingProfile.BreakOnNonKeyFrames,
                     maxAudioSampleRate,
                     maxAudioBitDepth,
                     null,
                     isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                    null,
                     maxAudioChannels,
                     null,
                     null,
@@ -281,6 +280,7 @@ namespace Jellyfin.Api.Controllers
                     null,
                     null,
                     null,
+                    SubtitleDeliveryMethod.Hls,
                     null,
                     null,
                     null,
@@ -295,10 +295,8 @@ namespace Jellyfin.Api.Controllers
                     mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
                     null,
                     null,
-                    null,
-                    null,
-                    null,
-                    null)
+                    EncodingContext.Static,
+                    new Dictionary<string, string>())
                     .ConfigureAwait(false);
             }
             else

From 8e05b226459eb0c513e227002f03068ca1e8dfc9 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 19:03:11 +0200
Subject: [PATCH 393/463] Use correct MediaInfo method

---
 .../Controllers/UniversalAudioController.cs        | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 0c7ef258a7..5360880524 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -6,6 +6,7 @@ using System.Net.Http;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.VideoDtos;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
@@ -151,7 +152,18 @@ namespace Jellyfin.Api.Controllers
             _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
 
             var mediaInfoController = new MediaInfoController(_mediaSourceManager, _deviceManager, _libraryManager, _networkManager, _mediaEncoder, _userManager, _authorizationContext, _loggerFactory.CreateLogger<MediaInfoController>(), _serverConfigurationManager);
-            var playbackInfoResult = await mediaInfoController.GetPlaybackInfo(itemId, userId).ConfigureAwait(false);
+            var playbackInfoResult = await mediaInfoController.GetPostedPlaybackInfo(
+                itemId,
+                userId,
+                maxStreamingBitrate,
+                startTimeTicks,
+                null,
+                null,
+                maxAudioChannels,
+                mediaSourceId,
+                null,
+                new DeviceProfileDto { DeviceProfile = deviceProfile })
+                .ConfigureAwait(false);
             var mediaSource = playbackInfoResult.Value.MediaSources[0];
 
             if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)

From a7eee4f4e52a9cdd94f56a6f5d57ab5abd453221 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 19:25:20 +0200
Subject: [PATCH 394/463] Remove duplicate code

---
 .../Controllers/UniversalAudioController.cs   | 103 ------------------
 1 file changed, 103 deletions(-)

diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 5360880524..44dd5f1fd9 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -204,58 +204,6 @@ namespace Jellyfin.Api.Controllers
                 if (isHeadRequest)
                 {
                     dynamicHlsController.Request.Method = HttpMethod.Head.Method;
-                    return await dynamicHlsController.GetMasterHlsAudioPlaylist(
-                        itemId,
-                        ".m3u8",
-                        isStatic,
-                        null,
-                        null,
-                        null,
-                        playbackInfoResult.Value.PlaySessionId,
-                        // fallback to mpegts if device reports some weird value unsupported by hls
-                        Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
-                        null,
-                        null,
-                        mediaSource.Id,
-                        deviceId,
-                        transcodingProfile.AudioCodec,
-                        null,
-                        null,
-                        null,
-                        transcodingProfile.BreakOnNonKeyFrames,
-                        maxAudioSampleRate,
-                        maxAudioBitDepth,
-                        null,
-                        isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                        maxAudioChannels,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        startTimeTicks,
-                        null,
-                        null,
-                        null,
-                        null,
-                        SubtitleDeliveryMethod.Hls,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                        null,
-                        null,
-                        EncodingContext.Static,
-                        new Dictionary<string, string>())
-                        .ConfigureAwait(false);
                 }
 
                 return await dynamicHlsController.GetMasterHlsAudioPlaylist(
@@ -331,57 +279,6 @@ namespace Jellyfin.Api.Controllers
                 if (isHeadRequest)
                 {
                     audioController.Request.Method = HttpMethod.Head.Method;
-                    return await audioController.GetAudioStream(
-                            itemId,
-                            isStatic ? null : ("." + mediaSource.TranscodingContainer),
-                            isStatic,
-                            null,
-                            null,
-                            null,
-                            playbackInfoResult.Value.PlaySessionId,
-                            null,
-                            null,
-                            null,
-                            mediaSource.Id,
-                            deviceId,
-                            audioCodec,
-                            null,
-                            null,
-                            null,
-                            breakOnNonKeyFrames,
-                            maxAudioSampleRate,
-                            maxAudioBitDepth,
-                            isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                            null,
-                            maxAudioChannels,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            startTimeTicks,
-                            null,
-                            null,
-                            null,
-                            null,
-                            SubtitleDeliveryMethod.Embed,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            null,
-                            mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                            null,
-                            null,
-                            null,
-                            null)
-                        .ConfigureAwait(false);
                 }
 
                 return await audioController.GetAudioStream(

From d2dd847b6042845f9c0146acb953d454f13dbe9c Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Sat, 1 Aug 2020 19:25:53 +0200
Subject: [PATCH 395/463] Remove duplicate code

---
 .../Controllers/UniversalAudioController.cs   | 98 +++++++++----------
 1 file changed, 49 insertions(+), 49 deletions(-)

diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 44dd5f1fd9..87d9a611a0 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -282,55 +282,55 @@ namespace Jellyfin.Api.Controllers
                 }
 
                 return await audioController.GetAudioStream(
-                        itemId,
-                        isStatic ? null : ("." + mediaSource.TranscodingContainer),
-                        isStatic,
-                        null,
-                        null,
-                        null,
-                        playbackInfoResult.Value.PlaySessionId,
-                        null,
-                        null,
-                        null,
-                        mediaSource.Id,
-                        deviceId,
-                        audioCodec,
-                        null,
-                        null,
-                        null,
-                        breakOnNonKeyFrames,
-                        maxAudioSampleRate,
-                        maxAudioBitDepth,
-                        isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
-                        null,
-                        maxAudioChannels,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        startTimeTicks,
-                        null,
-                        null,
-                        null,
-                        null,
-                        SubtitleDeliveryMethod.Embed,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        null,
-                        mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
-                        null,
-                        null,
-                        null,
-                        null)
+                    itemId,
+                    isStatic ? null : ("." + mediaSource.TranscodingContainer),
+                    isStatic,
+                    null,
+                    null,
+                    null,
+                    playbackInfoResult.Value.PlaySessionId,
+                    null,
+                    null,
+                    null,
+                    mediaSource.Id,
+                    deviceId,
+                    audioCodec,
+                    null,
+                    null,
+                    null,
+                    breakOnNonKeyFrames,
+                    maxAudioSampleRate,
+                    maxAudioBitDepth,
+                    isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
+                    null,
+                    maxAudioChannels,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    startTimeTicks,
+                    null,
+                    null,
+                    null,
+                    null,
+                    SubtitleDeliveryMethod.Embed,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    null,
+                    mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+                    null,
+                    null,
+                    null,
+                    null)
                     .ConfigureAwait(false);
             }
         }

From a6bc4c688d707c86aabc608cd03941df9b85e132 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 1 Aug 2020 14:52:45 -0400
Subject: [PATCH 396/463] Add using statement to DisplayPreferences migration

---
 .../Migrations/Routines/MigrateDisplayPreferencesDb.cs          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 183b1aeabd..780c1488dd 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Migrations.Routines
             var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
             using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
             {
-                var dbContext = _provider.CreateContext();
+                using var dbContext = _provider.CreateContext();
 
                 var results = connection.Query("SELECT * FROM userdisplaypreferences");
                 foreach (var result in results)

From ad32800504c08812212f7f39403924fdee635e5d Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 1 Aug 2020 14:56:32 -0400
Subject: [PATCH 397/463] Switch to unstable chromecast version.

---
 Jellyfin.Data/Enums/ChromecastVersion.cs             |  4 ++--
 .../Routines/MigrateDisplayPreferencesDb.cs          | 12 +++++++++---
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
index 855c75ab45..2622e08efb 100644
--- a/Jellyfin.Data/Enums/ChromecastVersion.cs
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -11,8 +11,8 @@
         Stable = 0,
 
         /// <summary>
-        /// Nightly Chromecast version.
+        /// Unstable Chromecast version.
         /// </summary>
-        Nightly = 1
+        Unstable = 1
     }
 }
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 780c1488dd..b15ccf01eb 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -64,6 +65,13 @@ namespace Jellyfin.Server.Migrations.Routines
                 HomeSectionType.None,
             };
 
+            var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase)
+            {
+                { "stable", ChromecastVersion.Stable },
+                { "nightly", ChromecastVersion.Unstable },
+                { "unstable", ChromecastVersion.Unstable }
+            };
+
             var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
             using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
             {
@@ -74,9 +82,7 @@ namespace Jellyfin.Server.Migrations.Routines
                 {
                     var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
                     var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
-                        ? Enum.TryParse<ChromecastVersion>(version, true, out var parsed)
-                            ? parsed
-                            : ChromecastVersion.Stable
+                        ? chromecastDict[version]
                         : ChromecastVersion.Stable;
 
                     var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())

From 6f306f0a17571eba7466a5f8bd591498d252e1be Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 1 Aug 2020 16:20:08 -0400
Subject: [PATCH 398/463] Minor fixes to ActivityManager

---
 Jellyfin.Server.Implementations/Activity/ActivityManager.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 65ceee32bf..2deefbe819 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -40,8 +40,9 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public async Task CreateAsync(ActivityLog entry)
         {
-            using var dbContext = _provider.CreateContext();
-            await dbContext.ActivityLogs.AddAsync(entry);
+            await using var dbContext = _provider.CreateContext();
+
+            dbContext.ActivityLogs.Add(entry);
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
 
             EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));

From 0f43780c8c50a6d933091056aaa523905f6e7b1a Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Sun, 2 Aug 2020 12:43:25 +0200
Subject: [PATCH 399/463] Apply suggestions from code review

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
---
 Jellyfin.Drawing.Skia/SkiaEncoder.cs         | 3 ++-
 Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 62e1d6ed14..a1caa751b1 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -487,7 +487,8 @@ namespace Jellyfin.Drawing.Skia
             var height = newImageSize.Height;
 
             // scale image (the FromImage creates a copy)
-            using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace)));
+            var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
+            using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
 
             // If all we're doing is resizing then we can stop now
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 3b35594865..b08c3750d7 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -176,7 +176,8 @@ namespace Jellyfin.Drawing.Skia
                     }
 
                     // Scale image. The FromBitmap creates a copy
-                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace)));
+                    var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
+                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo));
 
                     // draw this image into the strip at the next position
                     var xPos = x * cellWidth;

From aa32aba0f847241ec8660b1819f5ec769c312d0a Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Sun, 2 Aug 2020 23:30:34 +0200
Subject: [PATCH 400/463] Remove some unnecessary string allocations.

---
 .../Data/SqliteItemRepository.cs              | 133 +++---------------
 1 file changed, 20 insertions(+), 113 deletions(-)

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

From 85e43d657f2983a78f247ce1246fed22bf0aa185 Mon Sep 17 00:00:00 2001
From: Claus Vium <cvium@users.noreply.github.com>
Date: Sun, 2 Aug 2020 23:33:45 +0200
Subject: [PATCH 401/463] Update
 Emby.Server.Implementations/Data/SqliteItemRepository.cs

Co-authored-by: Bond-009 <bond.009@outlook.com>
---
 Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index b8901b99ee..e4ebdb87e7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -464,7 +464,7 @@ namespace Emby.Server.Implementations.Data
 
         private static readonly string _mediaAttachmentInsertPrefix;
 
-        private const string GetSaveItemCommandText =
+        private const string SaveItemCommandText =
             @"replace into TypedBaseItems
             (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
             values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";

From 996d0c07d02898774579e03ca9a32ef982707e19 Mon Sep 17 00:00:00 2001
From: Claus Vium <cvium@users.noreply.github.com>
Date: Sun, 2 Aug 2020 23:34:28 +0200
Subject: [PATCH 402/463] Update
 Emby.Server.Implementations/Data/SqliteItemRepository.cs

---
 Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index e4ebdb87e7..1c6d3cb94e 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -558,7 +558,7 @@ namespace Emby.Server.Implementations.Data
         {
             var statements = PrepareAll(db, new string[]
             {
-                GetSaveItemCommandText,
+                SaveItemCommandText,
                 "delete from AncestorIds where ItemId=@ItemId"
             }).ToList();
 

From e7d9d8f7c5289855ca16ebb35446b59cfc04c41a Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Sun, 2 Aug 2020 23:54:58 +0200
Subject: [PATCH 403/463] Change Budget and Revenue to long to avoid overflow

---
 .../Plugins/Tmdb/Models/Movies/MovieResult.cs                 | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
index 7566df8b61..704ebcd5a1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
 
         public BelongsToCollection Belongs_To_Collection { get; set; }
 
-        public int Budget { get; set; }
+        public long Budget { get; set; }
 
         public List<Genre> Genres { get; set; }
 
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
 
         public string Release_Date { get; set; }
 
-        public int Revenue { get; set; }
+        public long Revenue { get; set; }
 
         public int Runtime { get; set; }
 

From 705c0f93f671b54f19db4f446dd8bec0bc197fc0 Mon Sep 17 00:00:00 2001
From: Anthony Lavado <anthony@lavado.ca>
Date: Sun, 2 Aug 2020 19:56:54 -0400
Subject: [PATCH 404/463] Update to newer Jellyfin.XMLTV

---
 Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index efb2f81cc6..3b685c88b1 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -25,7 +25,7 @@
 
   <ItemGroup>
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
-    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
+    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />

From e7af1185eb0ea404989c6adc7b595e6f7669e77e Mon Sep 17 00:00:00 2001
From: michael9dk <61046533+michael9dk@users.noreply.github.com>
Date: Mon, 3 Aug 2020 13:21:44 +0200
Subject: [PATCH 405/463] Update README.md

Fix broken link to Azure DevOps pipeline.
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 3f1b5e5140..a41bfbca84 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ Note that it is also possible to [host the web client separately](#hosting-the-w
 
 There are three options to get the files for the web client.
 
-1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
+1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
 2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
 3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
 

From 17ba45963845fd8ec197e03dc154500059976e26 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2020 12:04:12 +0000
Subject: [PATCH 406/463] Bump PlaylistsNET from 1.0.6 to 1.1.2

Bumps [PlaylistsNET](https://github.com/tmk907/PlaylistsNET) from 1.0.6 to 1.1.2.
- [Release notes](https://github.com/tmk907/PlaylistsNET/releases)
- [Commits](https://github.com/tmk907/PlaylistsNET/commits)

Signed-off-by: dependabot[bot] <support@github.com>
---
 MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 42c7cec535..09879997a3 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,7 +19,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
-    <PackageReference Include="PlaylistsNET" Version="1.0.6" />
+    <PackageReference Include="PlaylistsNET" Version="1.1.2" />
     <PackageReference Include="TvDbSharper" Version="3.2.0" />
   </ItemGroup>
 

From be277e74d7e2ed61fcbc3d552020b9b9a125ce53 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2020 12:04:18 +0000
Subject: [PATCH 407/463] Bump Serilog.AspNetCore from 3.2.0 to 3.4.0

Bumps [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) from 3.2.0 to 3.4.0.
- [Release notes](https://github.com/serilog/serilog-aspnetcore/releases)
- [Commits](https://github.com/serilog/serilog-aspnetcore/compare/v3.2.0...v3.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
---
 Jellyfin.Server/Jellyfin.Server.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index b1bd38cffb..7541707d9f 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -45,7 +45,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
     <PackageReference Include="prometheus-net" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
-    <PackageReference Include="Serilog.AspNetCore" Version="3.2.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" />
     <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />

From 96817fca070bed7d718fbdd45ac0fe8e00c364d0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2020 12:04:27 +0000
Subject: [PATCH 408/463] Bump Mono.Nat from 2.0.1 to 2.0.2

Bumps [Mono.Nat](https://github.com/mono/Mono.Nat) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/mono/Mono.Nat/releases)
- [Commits](https://github.com/mono/Mono.Nat/compare/Mono.Nat-2.0.1...release-v2.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
---
 Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 3b685c88b1..e4774406ae 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -38,7 +38,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
-    <PackageReference Include="Mono.Nat" Version="2.0.1" />
+    <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
     <PackageReference Include="sharpcompress" Version="0.25.1" />

From d09fb5c53582a97c6da6660d6349d14da4a75c7a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2020 13:22:11 +0000
Subject: [PATCH 409/463] Bump sharpcompress from 0.25.1 to 0.26.0

Bumps [sharpcompress](https://github.com/adamhathcock/sharpcompress) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/adamhathcock/sharpcompress/releases)
- [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.25.1...0.26)

Signed-off-by: dependabot[bot] <support@github.com>
---
 Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index e4774406ae..a9eb66e18c 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -41,7 +41,7 @@
     <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
-    <PackageReference Include="sharpcompress" Version="0.25.1" />
+    <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.0.9" />
   </ItemGroup>

From b4c6ae9aba4153c042e6e98719b18c32abd608f7 Mon Sep 17 00:00:00 2001
From: dkanada <dkanada@users.noreply.github.com>
Date: Tue, 4 Aug 2020 01:02:02 +0900
Subject: [PATCH 410/463] disable compatibility checks for now

---
 .ci/azure-pipelines-abi.yml | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)

diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 635aa759ca..b558d2a6f0 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -12,10 +12,12 @@ parameters:
 jobs:
   - job: CompatibilityCheck
     displayName: Compatibility Check
+    dependsOn: Build
+    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     pool:
       vmImage: "${{ parameters.LinuxImage }}"
-    # only execute for pull requests
-    condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
+
     strategy:
       matrix:
         ${{ each Package in parameters.Packages }}:
@@ -23,7 +25,7 @@ jobs:
             NugetPackageName: ${{ Package.value.NugetPackageName }}
             AssemblyFileName: ${{ Package.value.AssemblyFileName }}
       maxParallel: 2
-    dependsOn: Build
+
     steps:
       - checkout: none
 
@@ -34,32 +36,33 @@ jobs:
           version: ${{ parameters.DotNetSdkVersion }}
 
       - task: DotNetCoreCLI@2
-        displayName: 'Install ABI CompatibilityChecker tool'
+        displayName: 'Install ABI CompatibilityChecker Tool'
         inputs:
           command: custom
           custom: tool
           arguments: 'update compatibilitychecker -g'
 
       - task: DownloadPipelineArtifact@2
-        displayName: "Download New Assembly Build Artifact"
+        displayName: 'Download New Assembly Build Artifact'
         inputs:
-          source: "current"
+          source: 'current'
           artifact: "$(NugetPackageName)"
           path: "$(System.ArtifactsDirectory)/new-artifacts"
           runVersion: "latest"
 
       - task: CopyFiles@2
-        displayName: "Copy New Assembly Build Artifact"
+        displayName: 'Copy New Assembly Build Artifact'
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
-          contents: "**/*.dll"
+          contents: '**/*.dll'
           targetFolder: $(System.ArtifactsDirectory)/new-release
           cleanTargetFolder: true
           overWrite: true
           flattenFolders: true
 
       - task: DownloadPipelineArtifact@2
-        displayName: "Download Reference Assembly Build Artifact"
+        displayName: 'Download Reference Assembly Build Artifact'
+        enabled: false
         inputs:
           source: "specific"
           artifact: "$(NugetPackageName)"
@@ -70,18 +73,19 @@ jobs:
           runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
 
       - task: CopyFiles@2
-        displayName: "Copy Reference Assembly Build Artifact"
+        displayName: 'Copy Reference Assembly Build Artifact'
+        enabled: false
         inputs:
           sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
-          contents: "**/*.dll"
+          contents: '**/*.dll'
           targetFolder: $(System.ArtifactsDirectory)/current-release
           cleanTargetFolder: true
           overWrite: true
           flattenFolders: true
 
-      # The `--warnings-only` switch will swallow the return code and not emit any errors.
       - task: DotNetCoreCLI@2
         displayName: 'Execute ABI Compatibility Check Tool'
+        enabled: false
         inputs:
           command: custom
           custom: compat

From dbeeb7cf4a715580432232c7098e4d86afccb37c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 12:01:24 -0600
Subject: [PATCH 411/463] fix merge conflicts

---
 .../DisplayPreferencesController.cs           | 133 ++++++++++--
 Jellyfin.Api/Controllers/MoviesController.cs  |   1 +
 .../Controllers/SubtitleController.cs         |  15 +-
 .../Controllers/SuggestionsController.cs      |   2 +-
 Jellyfin.Api/Controllers/TvShowsController.cs |   2 +-
 Jellyfin.Api/Controllers/UserController.cs    |   2 +-
 .../DisplayPreferencesDto.cs                  | 106 ++++++++++
 MediaBrowser.Api/DisplayPreferencesService.cs | 189 ------------------
 8 files changed, 236 insertions(+), 214 deletions(-)
 create mode 100644 Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
 delete mode 100644 MediaBrowser.Api/DisplayPreferencesService.cs

diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 1255e6dab0..62f6097f36 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,8 +1,12 @@
+using System;
 using System.ComponentModel.DataAnnotations;
 using System.Diagnostics.CodeAnalysis;
-using System.Threading;
+using System.Globalization;
+using System.Linq;
 using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Persistence;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -17,15 +21,15 @@ namespace Jellyfin.Api.Controllers
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DisplayPreferencesController : BaseJellyfinApiController
     {
-        private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
+        private readonly IDisplayPreferencesManager _displayPreferencesManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
         /// </summary>
-        /// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param>
-        public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository)
+        /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
+        public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
         {
-            _displayPreferencesRepository = displayPreferencesRepository;
+            _displayPreferencesManager = displayPreferencesManager;
         }
 
         /// <summary>
@@ -38,12 +42,47 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
         [HttpGet("{displayPreferencesId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<DisplayPreferences> GetDisplayPreferences(
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
+        public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
             [FromRoute] string? displayPreferencesId,
-            [FromQuery] [Required] string? userId,
+            [FromQuery] [Required] Guid userId,
             [FromQuery] [Required] string? client)
         {
-            return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
+            var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
+            var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
+
+            var dto = new DisplayPreferencesDto
+            {
+                Client = displayPreferences.Client,
+                Id = displayPreferences.UserId.ToString(),
+                ViewType = itemPreferences.ViewType.ToString(),
+                SortBy = itemPreferences.SortBy,
+                SortOrder = itemPreferences.SortOrder,
+                IndexBy = displayPreferences.IndexBy?.ToString(),
+                RememberIndexing = itemPreferences.RememberIndexing,
+                RememberSorting = itemPreferences.RememberSorting,
+                ScrollDirection = displayPreferences.ScrollDirection,
+                ShowBackdrop = displayPreferences.ShowBackdrop,
+                ShowSidebar = displayPreferences.ShowSidebar
+            };
+
+            foreach (var homeSection in displayPreferences.HomeSections)
+            {
+                dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
+            }
+
+            foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
+            {
+                dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
+            }
+
+            dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
+            dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
+            dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
+            dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
+            dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+
+            return dto;
         }
 
         /// <summary>
@@ -60,15 +99,77 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
         public ActionResult UpdateDisplayPreferences(
             [FromRoute] string? displayPreferencesId,
-            [FromQuery, BindRequired] string? userId,
+            [FromQuery, BindRequired] Guid userId,
             [FromQuery, BindRequired] string? client,
-            [FromBody, BindRequired] DisplayPreferences displayPreferences)
+            [FromBody, BindRequired] DisplayPreferencesDto displayPreferences)
         {
-            _displayPreferencesRepository.SaveDisplayPreferences(
-                displayPreferences,
-                userId,
-                client,
-                CancellationToken.None);
+            HomeSectionType[] defaults =
+            {
+                HomeSectionType.SmallLibraryTiles,
+                HomeSectionType.Resume,
+                HomeSectionType.ResumeAudio,
+                HomeSectionType.LiveTv,
+                HomeSectionType.NextUp,
+                HomeSectionType.LatestMedia, HomeSectionType.None,
+            };
+
+            var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
+            existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+            existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
+            existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
+
+            existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
+            existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+                ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
+                : ChromecastVersion.Stable;
+            existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+                ? bool.Parse(enableNextVideoInfoOverlay)
+                : true;
+            existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
+                ? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
+                : 10000;
+            existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
+                ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
+                : 30000;
+            existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
+                ? theme
+                : string.Empty;
+            existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
+                ? home
+                : string.Empty;
+            existingDisplayPreferences.HomeSections.Clear();
+
+            foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
+            {
+                var order = int.Parse(key.AsSpan().Slice("homesection".Length));
+                if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
+                {
+                    type = order < 7 ? defaults[order] : HomeSectionType.None;
+                }
+
+                existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
+            }
+
+            foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
+            {
+                var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
+                itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
+                _displayPreferencesManager.SaveChanges(itemPreferences);
+            }
+
+            var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
+            itemPrefs.SortBy = displayPreferences.SortBy;
+            itemPrefs.SortOrder = displayPreferences.SortOrder;
+            itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
+            itemPrefs.RememberSorting = displayPreferences.RememberSorting;
+
+            if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
+            {
+                itemPrefs.ViewType = viewType;
+            }
+
+            _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
+            _displayPreferencesManager.SaveChanges(itemPrefs);
 
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index a9af1a2c33..148d8a18e6 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 1c38b8de5c..b62ff80fcf 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -276,11 +276,12 @@ namespace Jellyfin.Api.Controllers
                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
             }
 
-            builder.AppendLine("#EXTM3U");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
-            builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+            builder.AppendLine("#EXTM3U")
+                .Append("#EXT-X-TARGETDURATION:")
+                .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+                .AppendLine("#EXT-X-VERSION:3")
+                .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
+                .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
 
             long positionTicks = 0;
 
@@ -291,7 +292,9 @@ namespace Jellyfin.Api.Controllers
                 var remaining = runtime - positionTicks;
                 var lengthTicks = Math.Min(remaining, segmentLengthTicks);
 
-                builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
+                builder.Append("#EXTINF:")
+                    .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
+                    .AppendLine(",");
 
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
 
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index bf3c1e2b14..55759f316f 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -2,11 +2,11 @@
 using System.Linq;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index d54bc10c03..508b5e24e4 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 8038ca0445..ce0c9281b9 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -455,7 +455,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
         {
-            var newUser = _userManager.CreateUser(request.Name);
+            var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
 
             // no need to authenticate password for new user
             if (request.Password != null)
diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
new file mode 100644
index 0000000000..249d828d33
--- /dev/null
+++ b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Api.Models.DisplayPreferencesDtos
+{
+    /// <summary>
+    /// Defines the display preferences for any item that supports them (usually Folders).
+    /// </summary>
+    public class DisplayPreferencesDto
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
+        /// </summary>
+        public DisplayPreferencesDto()
+        {
+            RememberIndexing = false;
+            PrimaryImageHeight = 250;
+            PrimaryImageWidth = 250;
+            ShowBackdrop = true;
+            CustomPrefs = new Dictionary<string, string>();
+        }
+
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public string? Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the view.
+        /// </summary>
+        /// <value>The type of the view.</value>
+        public string? ViewType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sort by.
+        /// </summary>
+        /// <value>The sort by.</value>
+        public string? SortBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index by.
+        /// </summary>
+        /// <value>The index by.</value>
+        public string? IndexBy { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [remember indexing].
+        /// </summary>
+        /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value>
+        public bool RememberIndexing { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height of the primary image.
+        /// </summary>
+        /// <value>The height of the primary image.</value>
+        public int PrimaryImageHeight { get; set; }
+
+        /// <summary>
+        /// Gets or sets the width of the primary image.
+        /// </summary>
+        /// <value>The width of the primary image.</value>
+        public int PrimaryImageWidth { get; set; }
+
+        /// <summary>
+        /// Gets the custom prefs.
+        /// </summary>
+        /// <value>The custom prefs.</value>
+        public Dictionary<string, string> CustomPrefs { get; }
+
+        /// <summary>
+        /// Gets or sets the scroll direction.
+        /// </summary>
+        /// <value>The scroll direction.</value>
+        public ScrollDirection ScrollDirection { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to show backdrops on this item.
+        /// </summary>
+        /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value>
+        public bool ShowBackdrop { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [remember sorting].
+        /// </summary>
+        /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value>
+        public bool RememberSorting { get; set; }
+
+        /// <summary>
+        /// Gets or sets the sort order.
+        /// </summary>
+        /// <value>The sort order.</value>
+        public SortOrder SortOrder { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [show sidebar].
+        /// </summary>
+        /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value>
+        public bool ShowSidebar { get; set; }
+
+        /// <summary>
+        /// Gets or sets the client.
+        /// </summary>
+        public string? Client { get; set; }
+    }
+}
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs
deleted file mode 100644
index 559b71efc7..0000000000
--- a/MediaBrowser.Api/DisplayPreferencesService.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class UpdateDisplayPreferences.
-    /// </summary>
-    [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
-    public class UpdateDisplayPreferences : DisplayPreferencesDto, IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "DisplayPreferencesId", Description = "DisplayPreferences Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string DisplayPreferencesId { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string UserId { get; set; }
-    }
-
-    [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
-    public class GetDisplayPreferences : IReturn<DisplayPreferencesDto>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string UserId { get; set; }
-
-        [ApiMember(Name = "Client", Description = "Client", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string Client { get; set; }
-    }
-
-    /// <summary>
-    /// Class DisplayPreferencesService.
-    /// </summary>
-    [Authenticated]
-    public class DisplayPreferencesService : BaseApiService
-    {
-        /// <summary>
-        /// The display preferences manager.
-        /// </summary>
-        private readonly IDisplayPreferencesManager _displayPreferencesManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
-        /// </summary>
-        /// <param name="displayPreferencesManager">The display preferences manager.</param>
-        public DisplayPreferencesService(
-            ILogger<DisplayPreferencesService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IDisplayPreferencesManager displayPreferencesManager)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            _displayPreferencesManager = displayPreferencesManager;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public object Get(GetDisplayPreferences request)
-        {
-            var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
-            var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
-
-            var dto = new DisplayPreferencesDto
-            {
-                Client = displayPreferences.Client,
-                Id = displayPreferences.UserId.ToString(),
-                ViewType = itemPreferences.ViewType.ToString(),
-                SortBy = itemPreferences.SortBy,
-                SortOrder = itemPreferences.SortOrder,
-                IndexBy = displayPreferences.IndexBy?.ToString(),
-                RememberIndexing = itemPreferences.RememberIndexing,
-                RememberSorting = itemPreferences.RememberSorting,
-                ScrollDirection = displayPreferences.ScrollDirection,
-                ShowBackdrop = displayPreferences.ShowBackdrop,
-                ShowSidebar = displayPreferences.ShowSidebar
-            };
-
-            foreach (var homeSection in displayPreferences.HomeSections)
-            {
-                dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
-            }
-
-            foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
-            {
-                dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
-            }
-
-            dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
-            dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString();
-            dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString();
-            dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString();
-            dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
-
-            return ToOptimizedResult(dto);
-        }
-
-        /// <summary>
-        /// Posts the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        public void Post(UpdateDisplayPreferences request)
-        {
-            HomeSectionType[] defaults =
-            {
-                HomeSectionType.SmallLibraryTiles,
-                HomeSectionType.Resume,
-                HomeSectionType.ResumeAudio,
-                HomeSectionType.LiveTv,
-                HomeSectionType.NextUp,
-                HomeSectionType.LatestMedia,
-                HomeSectionType.None,
-            };
-
-            var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
-
-            prefs.IndexBy = Enum.TryParse<IndexingKind>(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
-            prefs.ShowBackdrop = request.ShowBackdrop;
-            prefs.ShowSidebar = request.ShowSidebar;
-
-            prefs.ScrollDirection = request.ScrollDirection;
-            prefs.ChromecastVersion = request.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
-                ? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
-                : ChromecastVersion.Stable;
-            prefs.EnableNextVideoInfoOverlay = request.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
-                ? bool.Parse(enableNextVideoInfoOverlay)
-                : true;
-            prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
-            prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
-            prefs.DashboardTheme = request.CustomPrefs.TryGetValue("dashboardTheme", out var theme) ? theme : string.Empty;
-            prefs.TvHome = request.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty;
-            prefs.HomeSections.Clear();
-
-            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))
-            {
-                var order = int.Parse(key.AsSpan().Slice("homesection".Length));
-                if (!Enum.TryParse<HomeSectionType>(request.CustomPrefs[key], true, out var type))
-                {
-                    type = order < 7 ? defaults[order] : HomeSectionType.None;
-                }
-
-                prefs.HomeSections.Add(new HomeSection
-                {
-                    Order = order,
-                    Type = type
-                });
-            }
-
-            foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("landing-")))
-            {
-                var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Parse(key.Substring("landing-".Length)), prefs.Client);
-                itemPreferences.ViewType = Enum.Parse<ViewType>(request.ViewType);
-                _displayPreferencesManager.SaveChanges(itemPreferences);
-            }
-
-            var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Empty, prefs.Client);
-            itemPrefs.SortBy = request.SortBy;
-            itemPrefs.SortOrder = request.SortOrder;
-            itemPrefs.RememberIndexing = request.RememberIndexing;
-            itemPrefs.RememberSorting = request.RememberSorting;
-
-            if (Enum.TryParse<ViewType>(request.ViewType, true, out var viewType))
-            {
-                itemPrefs.ViewType = viewType;
-            }
-
-            _displayPreferencesManager.SaveChanges(prefs);
-            _displayPreferencesManager.SaveChanges(itemPrefs);
-        }
-    }
-}

From d53f7dd0c2e681328e387b788f4bb21d06e35d74 Mon Sep 17 00:00:00 2001
From: Alf Sebastian Houge <git@alfhouge.no>
Date: Mon, 3 Aug 2020 20:27:22 +0200
Subject: [PATCH 412/463] Add vscode launch configuration for launching without
 hosting webclient

---
 .vscode/launch.json | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0f698bfa4b..e4b1da6b1c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -15,6 +15,20 @@
             "stopAtEntry": false,
             "internalConsoleOptions": "openOnSessionStart"
         },
+        {
+            "name": ".NET Core Launch (nowebclient)",
+            "type": "coreclr",
+            "request": "launch",
+            "preLaunchTask": "build",
+            // If you have changed target frameworks, make sure to update the program path.
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+            "args": ["--nowebclient"],
+            "cwd": "${workspaceFolder}/Jellyfin.Server",
+            // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
+            "console": "internalConsole",
+            "stopAtEntry": false,
+            "internalConsoleOptions": "openOnSessionStart"
+        },
         {
             "name": ".NET Core Attach",
             "type": "coreclr",

From b2badb9906f3ccb84ce62c52066212e3e55bcd6f Mon Sep 17 00:00:00 2001
From: Alf Sebastian Houge <git@alfhouge.no>
Date: Mon, 3 Aug 2020 20:28:01 +0200
Subject: [PATCH 413/463] Fix links in README and note about setup wizard

Fix some of the web client links in the README and add a note about
hosting the web client seperately, and how the setup wizard can only be
run if the web client is not hosted separate
---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 3f1b5e5140..5128e5aea2 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ Note that it is also possible to [host the web client separately](#hosting-the-w
 
 There are three options to get the files for the web client.
 
-1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
+1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
 2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
 3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
 
@@ -166,3 +166,5 @@ To instruct the server not to host the web content, there is a `nowebclient` con
 switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
 
 Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
+
+**NOTE:** The setup wizard can not be run if the web client is hosted seperately.

From 2b355c36ff8328f962f607df4aa305e53f2e003e Mon Sep 17 00:00:00 2001
From: Bond_009 <Bond.009@outlook.com>
Date: Mon, 3 Aug 2020 20:32:45 +0200
Subject: [PATCH 414/463] Minor improvements

OFC I reduced some allocations
---
 .../Data/SqliteItemRepository.cs              | 58 ++++++++++---------
 .../Playback/Hls/BaseHlsService.cs            |  5 +-
 .../Playback/Hls/DynamicHlsService.cs         |  2 +-
 MediaBrowser.Api/Subtitles/SubtitleService.cs |  3 +-
 MediaBrowser.Model/Entities/MediaStream.cs    |  4 +-
 MediaBrowser.Providers/Manager/ImageSaver.cs  |  4 +-
 RSSDP/RSSDP.csproj                            |  2 -
 7 files changed, 41 insertions(+), 37 deletions(-)

diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 1c6d3cb94e..a246416283 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -978,7 +978,10 @@ namespace Emby.Server.Implementations.Data
                     continue;
                 }
 
-                str.Append($"{i.Key}={i.Value}|");
+                str.Append(i.Key)
+                    .Append('=')
+                    .Append(i.Value)
+                    .Append('|');
             }
 
             if (str.Length == 0)
@@ -1032,8 +1035,8 @@ namespace Emby.Server.Implementations.Data
                     continue;
                 }
 
-                str.Append(ToValueString(i))
-                    .Append('|');
+                AppendItemImageInfo(str, i);
+                str.Append('|');
             }
 
             str.Length -= 1; // Remove last |
@@ -1067,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
             item.ImageInfos = list.ToArray();
         }
 
-        public string ToValueString(ItemImageInfo image)
+        public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
         {
-            const string Delimeter = "*";
+            const char Delimeter = '*';
 
             var path = image.Path ?? string.Empty;
             var hash = image.BlurHash ?? string.Empty;
 
-            return GetPathToSave(path) +
-                   Delimeter +
-                   image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   image.Type +
-                   Delimeter +
-                   image.Width.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   image.Height.ToString(CultureInfo.InvariantCulture) +
-                   Delimeter +
-                   // Replace delimiters with other characters.
-                   // This can be removed when we migrate to a proper DB.
-                   hash.Replace('*', '/').Replace('|', '\\');
+            bldr.Append(GetPathToSave(path))
+                .Append(Delimeter)
+                .Append(image.DateModified.Ticks)
+                .Append(Delimeter)
+                .Append(image.Type)
+                .Append(Delimeter)
+                .Append(image.Width)
+                .Append(Delimeter)
+                .Append(image.Height)
+                .Append(Delimeter)
+                // Replace delimiters with other characters.
+                // This can be removed when we migrate to a proper DB.
+                .Append(hash.Replace('*', '/').Replace('|', '\\'));
         }
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -5659,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             const int Limit = 100;
             var startIndex = 0;
 
+            const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
+            var insertText = new StringBuilder(StartInsertText);
             while (startIndex < values.Count)
             {
-                var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
-
                 var endIndex = Math.Min(values.Count, startIndex + Limit);
 
                 for (var i = startIndex; i < endIndex; i++)
@@ -5704,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
 
                 startIndex += Limit;
+                insertText.Length = StartInsertText.Length;
             }
         }
 
@@ -5741,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             var startIndex = 0;
             var listIndex = 0;
 
+            const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
+            var insertText = new StringBuilder(StartInsertText);
             while (startIndex < people.Count)
             {
-                var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
-
                 var endIndex = Math.Min(people.Count, startIndex + Limit);
                 for (var i = startIndex; i < endIndex; i++)
                 {
@@ -5778,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
 
                 startIndex += Limit;
+                insertText.Length = StartInsertText.Length;
             }
         }
 
@@ -5893,10 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             const int Limit = 10;
             var startIndex = 0;
 
+            var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
             while (startIndex < streams.Count)
             {
-                var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
-
                 var endIndex = Math.Min(streams.Count, startIndex + Limit);
 
                 for (var i = startIndex; i < endIndex; i++)
@@ -5979,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 }
 
                 startIndex += Limit;
+                insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
             }
         }
 
@@ -6230,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
         {
             const int InsertAtOnce = 10;
 
+            var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
             for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
             {
-                var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
-
                 var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
 
                 for (var i = startIndex; i < endIndex; i++)
@@ -6279,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                     statement.Reset();
                     statement.MoveNext();
                 }
+
+                insertText.Length = _mediaAttachmentInsertPrefix.Length;
             }
         }
 
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index c80e8e64f7..3cdd80fad3 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -195,8 +195,9 @@ namespace MediaBrowser.Api.Playback.Hls
 
             // Main stream
             builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
-                .AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
-            var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
+                .AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture))
+                .Append("hls/");
+            var playlistUrl = Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
             builder.AppendLine(playlistUrl);
 
             return builder.ToString();
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index 661c1ba5f2..4d1473bdeb 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -1037,7 +1037,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 }
 
                 audioTranscodeParams.Add("-vn");
-                return string.Join(" ", audioTranscodeParams.ToArray());
+                return string.Join(" ", audioTranscodeParams);
             }
 
             if (EncodingHelper.IsCopyCodec(audioCodec))
diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs
index 6a6196d8a3..8dd9ca4a8b 100644
--- a/MediaBrowser.Api/Subtitles/SubtitleService.cs
+++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs
@@ -160,8 +160,6 @@ namespace MediaBrowser.Api.Subtitles
 
             var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
 
-            var builder = new StringBuilder();
-
             var runtime = mediaSource.RunTimeTicks ?? -1;
 
             if (runtime <= 0)
@@ -175,6 +173,7 @@ namespace MediaBrowser.Api.Subtitles
                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
             }
 
+            var builder = new StringBuilder();
             builder.AppendLine("#EXTM3U")
                 .Append("#EXT-X-TARGETDURATION:")
                 .AppendLine(request.SegmentLength.ToString(CultureInfo.InvariantCulture))
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 1b37cfc939..f9ec0d2388 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -233,7 +233,7 @@ namespace MediaBrowser.Model.Entities
 
                         if (!string.IsNullOrEmpty(Title))
                         {
-                           var result = new StringBuilder(Title);
+                            var result = new StringBuilder(Title);
                             foreach (var tag in attributes)
                             {
                                 // Keep Tags that are not already in Title.
@@ -246,7 +246,7 @@ namespace MediaBrowser.Model.Entities
                             return result.ToString();
                         }
 
-                        return string.Join(" - ", attributes.ToArray());
+                        return string.Join(" - ", attributes);
                     }
 
                     default:
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index f655b8edd2..32b543feff 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -125,7 +125,7 @@ namespace MediaBrowser.Providers.Manager
 
             // If there are more than one output paths, the stream will need to be seekable
             var memoryStream = new MemoryStream();
-            using (source)
+            await using (source.ConfigureAwait(false))
             {
                 await source.CopyToAsync(memoryStream).ConfigureAwait(false);
             }
@@ -138,7 +138,7 @@ namespace MediaBrowser.Providers.Manager
 
             var savedPaths = new List<string>();
 
-            await using (source)
+            await using (source.ConfigureAwait(false))
             {
                 var currentPathIndex = 0;
 
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index 5536931717..664663bd76 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -6,9 +6,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
   </ItemGroup>
 
   <PropertyGroup>

From 4980db159489ce009845dd109cb4b91dc809c72c Mon Sep 17 00:00:00 2001
From: Bond_009 <Bond.009@outlook.com>
Date: Mon, 3 Aug 2020 20:42:01 +0200
Subject: [PATCH 415/463] Fix spelling

---
 .../Data/SqliteItemRepository.cs                     | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a246416283..d11e5e62e3 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1072,21 +1072,21 @@ namespace Emby.Server.Implementations.Data
 
         public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
         {
-            const char Delimeter = '*';
+            const char Delimiter = '*';
 
             var path = image.Path ?? string.Empty;
             var hash = image.BlurHash ?? string.Empty;
 
             bldr.Append(GetPathToSave(path))
-                .Append(Delimeter)
+                .Append(Delimiter)
                 .Append(image.DateModified.Ticks)
-                .Append(Delimeter)
+                .Append(Delimiter)
                 .Append(image.Type)
-                .Append(Delimeter)
+                .Append(Delimiter)
                 .Append(image.Width)
-                .Append(Delimeter)
+                .Append(Delimiter)
                 .Append(image.Height)
-                .Append(Delimeter)
+                .Append(Delimiter)
                 // Replace delimiters with other characters.
                 // This can be removed when we migrate to a proper DB.
                 .Append(hash.Replace('*', '/').Replace('|', '\\'));

From 676f000286d0cdfe67fadf921e80003dc9321cd7 Mon Sep 17 00:00:00 2001
From: Alf Sebastian Houge <git@alfhouge.no>
Date: Mon, 3 Aug 2020 20:45:50 +0200
Subject: [PATCH 416/463] Remove comments from json file in .vscode/launch.json

---
 .vscode/launch.json | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.vscode/launch.json b/.vscode/launch.json
index e4b1da6b1c..bf1bd65cbe 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,11 +6,9 @@
             "type": "coreclr",
             "request": "launch",
             "preLaunchTask": "build",
-            // If you have changed target frameworks, make sure to update the program path.
             "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
             "args": [],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
-            // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
             "console": "internalConsole",
             "stopAtEntry": false,
             "internalConsoleOptions": "openOnSessionStart"
@@ -20,11 +18,9 @@
             "type": "coreclr",
             "request": "launch",
             "preLaunchTask": "build",
-            // If you have changed target frameworks, make sure to update the program path.
             "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
             "args": ["--nowebclient"],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
-            // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
             "console": "internalConsole",
             "stopAtEntry": false,
             "internalConsoleOptions": "openOnSessionStart"

From cfce1dba088dca44f5cf1ef3372738422a43d1c7 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 13:09:32 -0600
Subject: [PATCH 417/463] move WebSocket listeners to Jellyfin.Api

---
 .../ActivityLogWebSocketListener.cs           | 30 +++++----
 .../ScheduledTasksWebSocketListener.cs        | 65 ++++++++++---------
 .../SessionInfoWebSocketListener.cs           |  2 +-
 3 files changed, 53 insertions(+), 44 deletions(-)
 rename {MediaBrowser.Api/System => Jellyfin.Api/WebSocketListeners}/ActivityLogWebSocketListener.cs (75%)
 rename {MediaBrowser.Api/ScheduledTasks => Jellyfin.Api/WebSocketListeners}/ScheduledTasksWebSocketListener.cs (60%)
 rename {MediaBrowser.Api/Sessions => Jellyfin.Api/WebSocketListeners}/SessionInfoWebSocketListener.cs (98%)

diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
similarity index 75%
rename from MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
rename to Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 39976371a9..6395b8d62f 100644
--- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -5,34 +5,35 @@ using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Events;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Api.System
+namespace Jellyfin.Api.WebSocketListeners
 {
     /// <summary>
     /// Class SessionInfoWebSocketListener.
     /// </summary>
     public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
     {
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name => "ActivityLogEntry";
-
         /// <summary>
         /// The _kernel.
         /// </summary>
         private readonly IActivityManager _activityManager;
 
-        public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{ActivityLogWebSocketListener}"/> interface.</param>
+        /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+        public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager)
+            : base(logger)
         {
             _activityManager = activityManager;
             _activityManager.EntryCreated += OnEntryCreated;
         }
 
-        private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
-        {
-            SendData(true);
-        }
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        protected override string Name => "ActivityLogEntry";
 
         /// <summary>
         /// Gets the data to send.
@@ -50,5 +51,10 @@ namespace MediaBrowser.Api.System
 
             base.Dispose(dispose);
         }
+
+        private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        {
+            SendData(true);
+        }
     }
 }
diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
similarity index 60%
rename from MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs
rename to Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 25dd39f2de..12f815ff75 100644
--- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Api.ScheduledTasks
+namespace Jellyfin.Api.WebSocketListeners
 {
     /// <summary>
     /// Class ScheduledTasksWebSocketListener.
@@ -17,42 +17,27 @@ namespace MediaBrowser.Api.ScheduledTasks
         /// Gets or sets the task manager.
         /// </summary>
         /// <value>The task manager.</value>
-        private ITaskManager TaskManager { get; set; }
+        private readonly ITaskManager _taskManager;
 
         /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name => "ScheduledTasksInfo";
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener" /> class.
+        /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
         /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{ScheduledTasksWebSocketListener}"/> interface.</param>
+        /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
         public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager)
             : base(logger)
         {
-            TaskManager = taskManager;
+            _taskManager = taskManager;
 
-            TaskManager.TaskExecuting += TaskManager_TaskExecuting;
-            TaskManager.TaskCompleted += TaskManager_TaskCompleted;
+            _taskManager.TaskExecuting += OnTaskExecuting;
+            _taskManager.TaskCompleted += OnTaskCompleted;
         }
 
-        void TaskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
-        {
-            SendData(true);
-            e.Task.TaskProgress -= Argument_TaskProgress;
-        }
-
-        void TaskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
-        {
-            SendData(true);
-            e.Argument.TaskProgress += Argument_TaskProgress;
-        }
-
-        void Argument_TaskProgress(object sender, GenericEventArgs<double> e)
-        {
-            SendData(false);
-        }
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        protected override string Name => "ScheduledTasksInfo";
 
         /// <summary>
         /// Gets the data to send.
@@ -60,18 +45,36 @@ namespace MediaBrowser.Api.ScheduledTasks
         /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
         protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
         {
-            return Task.FromResult(TaskManager.ScheduledTasks
+            return Task.FromResult(_taskManager.ScheduledTasks
                 .OrderBy(i => i.Name)
                 .Select(ScheduledTaskHelpers.GetTaskInfo)
                 .Where(i => !i.IsHidden));
         }
 
+        /// <inheritdoc />
         protected override void Dispose(bool dispose)
         {
-            TaskManager.TaskExecuting -= TaskManager_TaskExecuting;
-            TaskManager.TaskCompleted -= TaskManager_TaskCompleted;
+            _taskManager.TaskExecuting -= OnTaskExecuting;
+            _taskManager.TaskCompleted -= OnTaskCompleted;
 
             base.Dispose(dispose);
         }
+
+        private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+        {
+            SendData(true);
+            e.Task.TaskProgress -= OnTaskProgress;
+        }
+
+        private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
+        {
+            SendData(true);
+            e.Argument.TaskProgress += OnTaskProgress;
+        }
+
+        private void OnTaskProgress(object sender, GenericEventArgs<double> e)
+        {
+            SendData(false);
+        }
     }
 }
diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
similarity index 98%
rename from MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
rename to Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 2400d6defe..ab9f789a16 100644
--- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using Microsoft.Extensions.Logging;
 
-namespace MediaBrowser.Api.Sessions
+namespace Jellyfin.Api.WebSocketListeners
 {
     /// <summary>
     /// Class SessionInfoWebSocketListener.

From db36b9d5016ee4608e23c21ad4445387ddb1acac Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 13:09:49 -0600
Subject: [PATCH 418/463] Delete MediaBrowser.Api

---
 MediaBrowser.Api/MediaBrowser.Api.csproj | 5 +++++
 MediaBrowser.sln                         | 6 ------
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index 3f75a3b296..6773bf2808 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -15,6 +15,11 @@
     <Compile Remove="Images\ImageService.cs" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="Sessions" />
+    <Folder Include="System" />
+  </ItemGroup>
+
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 0362eff1c8..75587da1f8 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -6,8 +6,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
@@ -80,10 +78,6 @@ Global
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
-		{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU
 		{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU

From 0f32beb75fe8ea31c957a067d165fa5bf8b280b3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 13:15:21 -0600
Subject: [PATCH 419/463] Properly use DI to reference other controllers.

---
 .../Controllers/TrailersController.cs         |  36 +-----
 .../Controllers/UniversalAudioController.cs   | 118 +++---------------
 2 files changed, 22 insertions(+), 132 deletions(-)

diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 645495551b..fbab7948ff 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,14 +1,10 @@
 using System;
 using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -18,32 +14,15 @@ namespace Jellyfin.Api.Controllers
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class TrailersController : BaseJellyfinApiController
     {
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<ItemsController> _logger;
-        private readonly IDtoService _dtoService;
-        private readonly ILocalizationManager _localizationManager;
+        private readonly ItemsController _itemsController;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TrailersController"/> class.
         /// </summary>
-        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
-        /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
-        public TrailersController(
-            ILoggerFactory loggerFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            ILocalizationManager localizationManager)
+        /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
+        public TrailersController(ItemsController itemsController)
         {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-            _localizationManager = localizationManager;
-            _logger = loggerFactory.CreateLogger<ItemsController>();
+            _itemsController = itemsController;
         }
 
         /// <summary>
@@ -214,12 +193,7 @@ namespace Jellyfin.Api.Controllers
         {
             var includeItemTypes = "Trailer";
 
-            return new ItemsController(
-                _userManager,
-                _libraryManager,
-                _localizationManager,
-                _dtoService,
-                _logger)
+            return _itemsController
                 .GetItems(
                     userId,
                     userId,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 87d9a611a0..50ab0ac054 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -7,21 +7,12 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.VideoDtos;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -30,72 +21,28 @@ namespace Jellyfin.Api.Controllers
     /// </summary>
     public class UniversalAudioController : BaseJellyfinApiController
     {
-        private readonly ILoggerFactory _loggerFactory;
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDeviceManager _deviceManager;
-        private readonly IDlnaManager _dlnaManager;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IFileSystem _fileSystem;
-        private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly INetworkManager _networkManager;
-        private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly IConfiguration _configuration;
-        private readonly ISubtitleEncoder _subtitleEncoder;
-        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly MediaInfoController _mediaInfoController;
+        private readonly DynamicHlsController _dynamicHlsController;
+        private readonly AudioController _audioController;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
         /// </summary>
-        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
-        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
-        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
-        /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> interface.</param>
-        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
-        /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
-        /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+        /// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
+        /// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
+        /// <param name="audioController">Instance of the <see cref="AudioController"/>.</param>
         public UniversalAudioController(
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager serverConfigurationManager,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
             IAuthorizationContext authorizationContext,
-            INetworkManager networkManager,
-            TranscodingJobHelper transcodingJobHelper,
-            IConfiguration configuration,
-            ISubtitleEncoder subtitleEncoder,
-            IHttpClientFactory httpClientFactory)
+            MediaInfoController mediaInfoController,
+            DynamicHlsController dynamicHlsController,
+            AudioController audioController)
         {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _mediaEncoder = mediaEncoder;
-            _fileSystem = fileSystem;
-            _dlnaManager = dlnaManager;
-            _deviceManager = deviceManager;
-            _mediaSourceManager = mediaSourceManager;
             _authorizationContext = authorizationContext;
-            _networkManager = networkManager;
-            _loggerFactory = loggerFactory;
-            _serverConfigurationManager = serverConfigurationManager;
-            _transcodingJobHelper = transcodingJobHelper;
-            _configuration = configuration;
-            _subtitleEncoder = subtitleEncoder;
-            _httpClientFactory = httpClientFactory;
+            _mediaInfoController = mediaInfoController;
+            _dynamicHlsController = dynamicHlsController;
+            _audioController = audioController;
         }
 
         /// <summary>
@@ -151,8 +98,7 @@ namespace Jellyfin.Api.Controllers
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
             _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
 
-            var mediaInfoController = new MediaInfoController(_mediaSourceManager, _deviceManager, _libraryManager, _networkManager, _mediaEncoder, _userManager, _authorizationContext, _loggerFactory.CreateLogger<MediaInfoController>(), _serverConfigurationManager);
-            var playbackInfoResult = await mediaInfoController.GetPostedPlaybackInfo(
+            var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
                 itemId,
                 userId,
                 maxStreamingBitrate,
@@ -180,21 +126,6 @@ namespace Jellyfin.Api.Controllers
             var isStatic = mediaSource.SupportsDirectStream;
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
-                var dynamicHlsController = new DynamicHlsController(
-                    _libraryManager,
-                    _userManager,
-                    _dlnaManager,
-                    _authorizationContext,
-                    _mediaSourceManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _networkManager,
-                    _loggerFactory.CreateLogger<DynamicHlsController>());
                 var transcodingProfile = deviceProfile.TranscodingProfiles[0];
 
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
@@ -203,10 +134,10 @@ namespace Jellyfin.Api.Controllers
 
                 if (isHeadRequest)
                 {
-                    dynamicHlsController.Request.Method = HttpMethod.Head.Method;
+                    _dynamicHlsController.Request.Method = HttpMethod.Head.Method;
                 }
 
-                return await dynamicHlsController.GetMasterHlsAudioPlaylist(
+                return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
                     itemId,
                     ".m3u8",
                     isStatic,
@@ -261,27 +192,12 @@ namespace Jellyfin.Api.Controllers
             }
             else
             {
-                var audioController = new AudioController(
-                    _dlnaManager,
-                    _userManager,
-                    _authorizationContext,
-                    _libraryManager,
-                    _mediaSourceManager,
-                    _serverConfigurationManager,
-                    _mediaEncoder,
-                    _fileSystem,
-                    _subtitleEncoder,
-                    _configuration,
-                    _deviceManager,
-                    _transcodingJobHelper,
-                    _httpClientFactory);
-
                 if (isHeadRequest)
                 {
-                    audioController.Request.Method = HttpMethod.Head.Method;
+                    _audioController.Request.Method = HttpMethod.Head.Method;
                 }
 
-                return await audioController.GetAudioStream(
+                return await _audioController.GetAudioStream(
                     itemId,
                     isStatic ? null : ("." + mediaSource.TranscodingContainer),
                     isStatic,

From 8bb510a9f65c73c08fb692a86112e1888c8206ce Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 13:16:29 -0600
Subject: [PATCH 420/463] Specify Logger type

---
 Jellyfin.Api/Controllers/ItemsController.cs     | 2 +-
 Jellyfin.Api/Controllers/MediaInfoController.cs | 2 +-
 Jellyfin.Api/Controllers/VideoHlsController.cs  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 41fe47db10..49fb9238f1 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
         private readonly IDtoService _dtoService;
-        private readonly ILogger _logger;
+        private readonly ILogger<ItemsController> _logger;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemsController"/> class.
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index c2c02c02ca..57aff21b0a 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IUserManager _userManager;
         private readonly IAuthorizationContext _authContext;
-        private readonly ILogger _logger;
+        private readonly ILogger<MediaInfoController> _logger;
         private readonly IServerConfigurationManager _serverConfigurationManager;
 
         /// <summary>
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 3f8a2048e4..8520dd1638 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IConfiguration _configuration;
         private readonly IDeviceManager _deviceManager;
         private readonly TranscodingJobHelper _transcodingJobHelper;
-        private readonly ILogger _logger;
+        private readonly ILogger<VideoHlsController> _logger;
         private readonly EncodingOptions _encodingOptions;
 
         /// <summary>

From 1535f363b28ab7e57354f2724f5f1900a000b5cc Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 13:33:43 -0600
Subject: [PATCH 421/463] Fix some request parameters

---
 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs |  3 +-
 .../Controllers/LibraryStructureController.cs |  4 +-
 Jellyfin.Api/Controllers/LiveTvController.cs  |  1 -
 .../Controllers/MediaInfoController.cs        | 13 ++--
 Jellyfin.Api/Helpers/RequestHelpers.cs        |  1 -
 .../Models/MediaInfoDtos/OpenLiveStreamDto.cs | 24 ++++++++
 .../SessionInfoWebSocketListener.cs           | 60 +++++++++----------
 7 files changed, 61 insertions(+), 45 deletions(-)
 create mode 100644 Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs

diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 495ff9d128..aa366f5672 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -1,5 +1,4 @@
-using System.Net;
-using System.Security.Claims;
+using System.Security.Claims;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Net;
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index b7f3c9b07c..827879e0a8 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateMediaPath(
             [FromQuery] string? name,
-            [FromQuery] MediaPathInfo? pathInfo)
+            [FromBody] MediaPathInfo? pathInfo)
         {
             if (string.IsNullOrWhiteSpace(name))
             {
@@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateLibraryOptions(
             [FromQuery] string? id,
-            [FromQuery] LibraryOptions? libraryOptions)
+            [FromBody] LibraryOptions? libraryOptions)
         {
             var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
 
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 9144d6f285..bbe5544f93 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Querying;
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 57aff21b0a..5b0f46b02e 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -7,6 +7,7 @@ using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.MediaInfoDtos;
 using Jellyfin.Api.Models.VideoDtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
@@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
         {
-            return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
+            return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -231,8 +232,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
         /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
         /// <param name="itemId">The item id.</param>
-        /// <param name="deviceProfile">The device profile.</param>
-        /// <param name="directPlayProtocols">The direct play protocols. Default: <see cref="MediaProtocol.Http"/>.</param>
+        /// <param name="openLiveStreamDto">The open live stream dto.</param>
         /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
         /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
         /// <response code="200">Media source opened.</response>
@@ -249,8 +249,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? maxAudioChannels,
             [FromQuery] Guid? itemId,
-            [FromQuery] DeviceProfile? deviceProfile,
-            [FromQuery] MediaProtocol[] directPlayProtocols,
+            [FromBody] OpenLiveStreamDto openLiveStreamDto,
             [FromQuery] bool enableDirectPlay = true,
             [FromQuery] bool enableDirectStream = true)
         {
@@ -265,10 +264,10 @@ namespace Jellyfin.Api.Controllers
                 SubtitleStreamIndex = subtitleStreamIndex,
                 MaxAudioChannels = maxAudioChannels,
                 ItemId = itemId ?? Guid.Empty,
-                DeviceProfile = deviceProfile,
+                DeviceProfile = openLiveStreamDto?.DeviceProfile,
                 EnableDirectPlay = enableDirectPlay,
                 EnableDirectStream = enableDirectStream,
-                DirectPlayProtocols = directPlayProtocols ?? new[] { MediaProtocol.Http }
+                DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
             };
             return await OpenMediaSource(request).ConfigureAwait(false);
         }
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index d9e993d496..fbaa692700 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -5,7 +5,6 @@ using System.Net;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Http;
 
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
new file mode 100644
index 0000000000..f797a38076
--- /dev/null
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.MediaInfo;
+
+namespace Jellyfin.Api.Models.MediaInfoDtos
+{
+    /// <summary>
+    /// Open live stream dto.
+    /// </summary>
+    public class OpenLiveStreamDto
+    {
+        /// <summary>
+        /// Gets or sets the device profile.
+        /// </summary>
+        public DeviceProfile? DeviceProfile { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device play protocols.
+        /// </summary>
+        [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
+        [SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
+        public MediaProtocol[]? DirectPlayProtocols { get; set; }
+    }
+}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index ab9f789a16..1fb5dc412c 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -12,20 +12,13 @@ namespace Jellyfin.Api.WebSocketListeners
     /// </summary>
     public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
     {
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name => "Sessions";
-
-        /// <summary>
-        /// The _kernel.
-        /// </summary>
         private readonly ISessionManager _sessionManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
         /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{SessionInfoWebSocketListener}"/> interface.</param>
+        /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
         public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager)
             : base(logger)
         {
@@ -40,6 +33,32 @@ namespace Jellyfin.Api.WebSocketListeners
             _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
         }
 
+        /// <inheritdoc />
+        protected override string Name => "Sessions";
+
+        /// <summary>
+        /// Gets the data to send.
+        /// </summary>
+        /// <returns>Task{SystemInfo}.</returns>
+        protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
+        {
+            return Task.FromResult(_sessionManager.Sessions);
+        }
+
+        /// <inheritdoc />
+        protected override void Dispose(bool dispose)
+        {
+            _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+            _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+            _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+            _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+            _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+            _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+
+            base.Dispose(dispose);
+        }
+
         private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
         {
             await SendData(false).ConfigureAwait(false);
@@ -74,28 +93,5 @@ namespace Jellyfin.Api.WebSocketListeners
         {
             await SendData(true).ConfigureAwait(false);
         }
-
-        /// <summary>
-        /// Gets the data to send.
-        /// </summary>
-        /// <returns>Task{SystemInfo}.</returns>
-        protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
-        {
-            return Task.FromResult(_sessionManager.Sessions);
-        }
-
-        /// <inheritdoc />
-        protected override void Dispose(bool dispose)
-        {
-            _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
-            _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
-            _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
-            _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
-            _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
-            _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
-            _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
-
-            base.Dispose(dispose);
-        }
     }
 }

From 9e00aa3014c0044c0918a775c3394763666b30af Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 14:38:51 -0600
Subject: [PATCH 422/463] fix openapi validation errors

---
 Jellyfin.Api/Controllers/AudioController.cs   |  8 ++--
 .../Controllers/BrandingController.cs         |  2 +-
 .../Controllers/DlnaServerController.cs       | 12 ++---
 .../Controllers/DynamicHlsController.cs       |  4 +-
 .../Controllers/HlsSegmentController.cs       |  4 +-
 Jellyfin.Api/Controllers/ImageController.cs   | 28 ++++++------
 Jellyfin.Api/Controllers/ItemsController.cs   |  2 +-
 Jellyfin.Api/Controllers/LibraryController.cs | 14 +++---
 Jellyfin.Api/Controllers/LiveTvController.cs  |  6 +--
 Jellyfin.Api/Controllers/SessionController.cs |  2 +-
 Jellyfin.Api/Controllers/StartupController.cs |  4 +-
 .../Controllers/SubtitleController.cs         |  2 +-
 .../Controllers/SyncPlayController.cs         | 18 ++++----
 Jellyfin.Api/Controllers/SystemController.cs  |  4 +-
 .../Controllers/UniversalAudioController.cs   |  6 +--
 Jellyfin.Api/Controllers/VideosController.cs  |  6 +--
 .../ApiServiceCollectionExtensions.cs         | 11 ++++-
 tests/Jellyfin.Api.Tests/GetPathValueTests.cs | 45 -------------------
 18 files changed, 70 insertions(+), 108 deletions(-)
 delete mode 100644 tests/Jellyfin.Api.Tests/GetPathValueTests.cs

diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index ebae1caa0e..4de87616c5 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -144,10 +144,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/{stream=stream}.{container?}")]
-        [HttpGet("{itemId}/stream")]
-        [HttpHead("{itemId}/{stream=stream}.{container?}")]
-        [HttpHead("{itemId}/stream")]
+        [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")]
+        [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")]
+        [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetAudioStream(
             [FromRoute] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index 67790c0e4a..1d4836f278 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NoContentResult"/> if the css is not configured.
         /// </returns>
         [HttpGet("Css")]
-        [HttpGet("Css.css")]
+        [HttpGet("Css.css", Name = "GetBrandingCss_2")]
         [Produces("text/css")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 2f5561adb9..ef507f2ed6 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <response code="200">Description xml returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
-        [HttpGet("{serverId}/description.xml")]
         [HttpGet("{serverId}/description")]
+        [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult GetDescriptionXml([FromRoute] string serverId)
@@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <response code="200">Dlna content directory returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
-        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")]
         [HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
         [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
-        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")]
         [HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")]
         [Produces(XMLContentType)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <param name="fileName">The icon filename.</param>
         /// <returns>Icon stream.</returns>
-        [HttpGet("{serverId}/icons/{filename}")]
+        [HttpGet("{serverId}/icons/{fileName}")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
         {
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="fileName">The icon filename.</param>
         /// <returns>Icon stream.</returns>
-        [HttpGet("icons/{filename}")]
+        [HttpGet("icons/{fileName}")]
         public ActionResult GetIcon([FromRoute] string fileName)
         {
             return GetIconInternal(fileName);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index b7e1837c97..c4f79ce950 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -165,7 +165,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
         [HttpGet("/Videos/{itemId}/master.m3u8")]
-        [HttpHead("/Videos/{itemId}/master.m3u8")]
+        [HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetMasterHlsVideoPlaylist(
             [FromRoute] Guid itemId,
@@ -335,7 +335,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
         [HttpGet("/Audio/{itemId}/master.m3u8")]
-        [HttpHead("/Audio/{itemId}/master.m3u8")]
+        [HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetMasterHlsAudioPlaylist(
             [FromRoute] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index efdb6a3691..7bf9326a71 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -50,8 +50,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
         // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
         // [Authenticated]
-        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")]
-        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")]
+        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
+        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
         public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 18220c5f34..3a445b1b3c 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("/Users/{userId}/Images/{imageType}")]
-        [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")]
+        [HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("/Users/{userId}/Images/{itemType}")]
-        [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")]
+        [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -167,7 +167,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpDelete("/Items/{itemId}/Images/{imageType}")]
-        [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpPost("/Items/{itemId}/Images/{imageType}")]
-        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -342,9 +342,9 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Items/{itemId}/Images/{imageType}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}")]
-        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
+        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetItemImage(
@@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetItemImage2(
@@ -500,7 +500,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetArtistImage(
@@ -578,7 +578,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetGenreImage(
@@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetMusicGenreImage(
@@ -734,7 +734,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetPersonImage(
@@ -812,7 +812,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetStudioImage(
@@ -890,7 +890,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
         [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetUserImage(
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 49fb9238f1..354741ced1 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional, include image information in output.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
         [HttpGet("/Items")]
-        [HttpGet("/Users/{uId}/Items")]
+        [HttpGet("/Users/{uId}/Items", Name = "GetItems_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
             [FromRoute] Guid? uId,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 5ad466c557..0ec7e2b8c0 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -521,7 +521,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="tvdbId">The tvdbId.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Library/Series/Added")]
+        [HttpPost("/Library/Series/Added", Name = "PostAddedSeries")]
         [HttpPost("/Library/Series/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -551,7 +551,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imdbId">The imdbId.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Library/Movies/Added")]
+        [HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")]
         [HttpPost("/Library/Movies/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -679,12 +679,12 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
         /// <response code="200">Similar items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
-        [HttpGet("/Artists/{itemId}/Similar")]
+        [HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
         [HttpGet("/Items/{itemId}/Similar")]
-        [HttpGet("/Albums/{itemId}/Similar")]
-        [HttpGet("/Shows/{itemId}/Similar")]
-        [HttpGet("/Movies/{itemId}/Similar")]
-        [HttpGet("/Trailers/{itemId}/Similar")]
+        [HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
+        [HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
+        [HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
+        [HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index bbe5544f93..89112eea7d 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Channels")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+        public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
             [FromQuery] ChannelType? type,
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
@@ -535,7 +535,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Programs")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms(
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
             [FromQuery] string? channelIds,
             [FromQuery] Guid? userId,
             [FromQuery] DateTime? minStartDate,
@@ -933,7 +933,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Obsolete("This endpoint is obsolete.")]
-        public ActionResult<BaseItemDto> GetRecordingGroup([FromQuery] Guid? groupId)
+        public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute] Guid? groupId)
         {
             return NotFound();
         }
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 0c98a8e711..1b300e0d8a 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="command">The command to send.</param>
         /// <response code="204">General command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Command/{Command}")]
+        [HttpPost("/Sessions/{sessionId}/Command/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
             [FromRoute] string? sessionId,
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index f9e4e61b5e..c8e3cc4f52 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Initial user retrieved.</response>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
-        [HttpGet("FirstUser")]
+        [HttpGet("FirstUser", Name = "GetFirstUser_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<StartupUserDto> GetFirstUser()
         {
@@ -131,7 +131,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpPost("User")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
+        public async Task<ActionResult> UpdateStartupUser([FromForm] StartupUserDto startupUserDto)
         {
             var user = _userManager.Users.First();
 
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index b62ff80fcf..f8c19d15c4 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
         [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
-        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")]
+        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitle(
             [FromRoute, Required] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 55ed42227d..2b1b95b1b5 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("New")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CreateNewGroup()
+        public ActionResult SyncPlayCreateGroup()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Join")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult JoinGroup([FromQuery, Required] Guid groupId)
+        public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
 
@@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Leave")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult LeaveGroup()
+        public ActionResult SyncPlayLeaveGroup()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
         [HttpGet("List")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<IEnumerable<GroupInfoView>> GetSyncPlayGroups([FromQuery] Guid? filterItemId)
+        public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
@@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Play")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult Play()
+        public ActionResult SyncPlayPlay()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
@@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Pause")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult Pause()
+        public ActionResult SyncPlayPause()
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
@@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Seek")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult Seek([FromQuery] long positionTicks)
+        public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Buffering")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
+        public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("Ping")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult Ping([FromQuery] double ping)
+        public ActionResult SyncPlayPing([FromQuery] double ping)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
             var syncPlayRequest = new PlaybackRequest()
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bc606f7aad..e0bce3a417 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Information retrieved.</response>
         /// <returns>The server name.</returns>
-        [HttpGet("Ping")]
-        [HttpPost("Ping")]
+        [HttpGet("Ping", Name = "GetPingSystem")]
+        [HttpPost("Ping", Name = "PostPingSystem")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<string> PingSystem()
         {
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 50ab0ac054..5a9bec2b05 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -69,9 +69,9 @@ 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=universal}.{container?}")]
-        [HttpHead("/Audio/{itemId}/universal")]
-        [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")]
+        [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")]
+        [HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
+        [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status302Found)]
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index d1ef817eb6..ebe88a9c05 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -316,10 +316,10 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/{stream=stream}.{container?}")]
+        [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")]
         [HttpGet("{itemId}/stream")]
-        [HttpHead("{itemId}/{stream=stream}.{container?}")]
-        [HttpHead("{itemId}/stream")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+        [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetVideoStream(
             [FromRoute] Guid itemId,
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index cfbabf7954..6e91042dfd 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -198,8 +198,15 @@ namespace Jellyfin.Server.Extensions
                     $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 
                 // Use method name as operationId
-                c.CustomOperationIds(description =>
-                    description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
+                c.CustomOperationIds(
+                    description =>
+                    {
+                        description.TryGetMethodInfo(out MethodInfo methodInfo);
+                        // Attribute name, method name, none.
+                        return description?.ActionDescriptor?.AttributeRouteInfo?.Name
+                               ?? methodInfo?.Name
+                               ?? null;
+                    });
 
                 // TODO - remove when all types are supported in System.Text.Json
                 c.AddSwaggerTypeMappings();
diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
deleted file mode 100644
index 397eb2edc3..0000000000
--- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using MediaBrowser.Api;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests
-{
-    public class GetPathValueTests
-    {
-        [Theory]
-        [InlineData("https://localhost:8096/ScheduledTasks/1234/Triggers", "", 1, "1234")]
-        [InlineData("https://localhost:8096/emby/ScheduledTasks/1234/Triggers", "", 1, "1234")]
-        [InlineData("https://localhost:8096/mediabrowser/ScheduledTasks/1234/Triggers", "", 1, "1234")]
-        [InlineData("https://localhost:8096/jellyfin/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        [InlineData("https://localhost:8096/jellyfin/2/emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        [InlineData("https://localhost:8096/jellyfin/2/mediabrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        [InlineData("https://localhost:8096/JELLYFIN/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        [InlineData("https://localhost:8096/JELLYFIN/2/Emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        [InlineData("https://localhost:8096/JELLYFIN/2/MediaBrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
-        public void GetPathValueTest(string path, string baseUrl, int index, string value)
-        {
-            var reqMock = Mock.Of<IRequest>(x => x.PathInfo == path);
-            var conf = new ServerConfiguration()
-            {
-                BaseUrl = baseUrl
-            };
-
-            var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf);
-
-            var service = new TestService(
-                new NullLogger<TestService>(),
-                confManagerMock,
-                Mock.Of<IHttpResultFactory>())
-            {
-                Request = reqMock
-            };
-
-            Assert.Equal(value, service.GetPathValue(index).ToString());
-        }
-    }
-}

From e64924f4d385a53c94f5b884be10d8d56a2f73a9 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 14:42:46 -0600
Subject: [PATCH 423/463] actually remove MediaBrowser.Api

---
 MediaBrowser.Api/ApiEntryPoint.cs             |  678 ---------
 MediaBrowser.Api/BaseApiService.cs            |  416 ------
 MediaBrowser.Api/IHasDtoOptions.cs            |   13 -
 MediaBrowser.Api/IHasItemFields.cs            |   49 -
 MediaBrowser.Api/MediaBrowser.Api.csproj      |   29 -
 .../Playback/BaseStreamingService.cs          | 1008 --------------
 .../Playback/Hls/BaseHlsService.cs            |  344 -----
 .../Playback/Hls/DynamicHlsService.cs         | 1226 -----------------
 .../Playback/Hls/HlsCodecStringFactory.cs     |  126 --
 .../Playback/Hls/VideoHlsService.cs           |    6 -
 .../BaseProgressiveStreamingService.cs        |  442 ------
 .../Progressive/ProgressiveStreamWriter.cs    |  182 ---
 .../Playback/Progressive/VideoService.cs      |   88 --
 .../Playback/StaticRemoteStreamWriter.cs      |   44 -
 MediaBrowser.Api/Playback/StreamRequest.cs    |   37 -
 MediaBrowser.Api/Playback/StreamState.cs      |  143 --
 .../Playback/TranscodingThrottler.cs          |  175 ---
 MediaBrowser.Api/Properties/AssemblyInfo.cs   |   23 -
 MediaBrowser.Api/TestService.cs               |   26 -
 MediaBrowser.Api/TranscodingJob.cs            |  165 ---
 20 files changed, 5220 deletions(-)
 delete mode 100644 MediaBrowser.Api/ApiEntryPoint.cs
 delete mode 100644 MediaBrowser.Api/BaseApiService.cs
 delete mode 100644 MediaBrowser.Api/IHasDtoOptions.cs
 delete mode 100644 MediaBrowser.Api/IHasItemFields.cs
 delete mode 100644 MediaBrowser.Api/MediaBrowser.Api.csproj
 delete mode 100644 MediaBrowser.Api/Playback/BaseStreamingService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
 delete mode 100644 MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
 delete mode 100644 MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
 delete mode 100644 MediaBrowser.Api/Playback/Progressive/VideoService.cs
 delete mode 100644 MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
 delete mode 100644 MediaBrowser.Api/Playback/StreamRequest.cs
 delete mode 100644 MediaBrowser.Api/Playback/StreamState.cs
 delete mode 100644 MediaBrowser.Api/Playback/TranscodingThrottler.cs
 delete mode 100644 MediaBrowser.Api/Properties/AssemblyInfo.cs
 delete mode 100644 MediaBrowser.Api/TestService.cs
 delete mode 100644 MediaBrowser.Api/TranscodingJob.cs

diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs
deleted file mode 100644
index b041effb2e..0000000000
--- a/MediaBrowser.Api/ApiEntryPoint.cs
+++ /dev/null
@@ -1,678 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Api.Playback;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class ServerEntryPoint.
-    /// </summary>
-    public class ApiEntryPoint : IServerEntryPoint
-    {
-        /// <summary>
-        /// The instance.
-        /// </summary>
-        public static ApiEntryPoint Instance;
-
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private ILogger<ApiEntryPoint> _logger;
-
-        /// <summary>
-        /// The configuration manager.
-        /// </summary>
-        private IServerConfigurationManager _serverConfigurationManager;
-
-        private readonly ISessionManager _sessionManager;
-        private readonly IFileSystem _fileSystem;
-        private readonly IMediaSourceManager _mediaSourceManager;
-
-        /// <summary>
-        /// The active transcoding jobs.
-        /// </summary>
-        private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
-
-        private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
-            new Dictionary<string, SemaphoreSlim>();
-
-        private bool _disposed = false;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="sessionManager">The session manager.</param>
-        /// <param name="config">The configuration.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="mediaSourceManager">The media source manager.</param>
-        public ApiEntryPoint(
-            ILogger<ApiEntryPoint> logger,
-            ISessionManager sessionManager,
-            IServerConfigurationManager config,
-            IFileSystem fileSystem,
-            IMediaSourceManager mediaSourceManager)
-        {
-            _logger = logger;
-            _sessionManager = sessionManager;
-            _serverConfigurationManager = config;
-            _fileSystem = fileSystem;
-            _mediaSourceManager = mediaSourceManager;
-
-            _sessionManager.PlaybackProgress += OnPlaybackProgress;
-            _sessionManager.PlaybackStart += OnPlaybackStart;
-
-            Instance = this;
-        }
-
-        public static string[] Split(string value, char separator, bool removeEmpty)
-        {
-            if (string.IsNullOrWhiteSpace(value))
-            {
-                return Array.Empty<string>();
-            }
-
-            return removeEmpty
-                ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
-                : value.Split(separator);
-        }
-
-        public SemaphoreSlim GetTranscodingLock(string outputPath)
-        {
-            lock (_transcodingLocks)
-            {
-                if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
-                {
-                    result = new SemaphoreSlim(1, 1);
-                    _transcodingLocks[outputPath] = result;
-                }
-
-                return result;
-            }
-        }
-
-        private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
-        {
-            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
-            {
-                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
-            }
-        }
-
-        private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
-        {
-            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
-            {
-                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
-            }
-        }
-
-        /// <summary>
-        /// Runs this instance.
-        /// </summary>
-        public Task RunAsync()
-        {
-            try
-            {
-                DeleteEncodedMediaCache();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error deleting encoded media cache");
-            }
-
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Deletes the encoded media cache.
-        /// </summary>
-        private void DeleteEncodedMediaCache()
-        {
-            var path = _serverConfigurationManager.GetTranscodePath();
-            if (!Directory.Exists(path))
-            {
-                return;
-            }
-
-            foreach (var file in _fileSystem.GetFilePaths(path, true))
-            {
-                _fileSystem.DeleteFile(file);
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (dispose)
-            {
-                // TODO: dispose
-            }
-
-            var jobs = _activeTranscodingJobs.ToList();
-            var jobCount = jobs.Count;
-
-            IEnumerable<Task> GetKillJobs()
-            {
-                foreach (var job in jobs)
-                {
-                    yield return KillTranscodingJob(job, false, path => true);
-                }
-            }
-
-            // Wait for all processes to be killed
-            if (jobCount > 0)
-            {
-                Task.WaitAll(GetKillJobs().ToArray());
-            }
-
-            _activeTranscodingJobs.Clear();
-            _transcodingLocks.Clear();
-
-            _sessionManager.PlaybackProgress -= OnPlaybackProgress;
-            _sessionManager.PlaybackStart -= OnPlaybackStart;
-
-            _disposed = true;
-        }
-
-
-        /// <summary>
-        /// Called when [transcode beginning].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="playSessionId">The play session identifier.</param>
-        /// <param name="liveStreamId">The live stream identifier.</param>
-        /// <param name="transcodingJobId">The transcoding job identifier.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="process">The process.</param>
-        /// <param name="deviceId">The device id.</param>
-        /// <param name="state">The state.</param>
-        /// <param name="cancellationTokenSource">The cancellation token source.</param>
-        /// <returns>TranscodingJob.</returns>
-        public TranscodingJob OnTranscodeBeginning(
-            string path,
-            string playSessionId,
-            string liveStreamId,
-            string transcodingJobId,
-            TranscodingJobType type,
-            Process process,
-            string deviceId,
-            StreamState state,
-            CancellationTokenSource cancellationTokenSource)
-        {
-            lock (_activeTranscodingJobs)
-            {
-                var job = new TranscodingJob(_logger)
-                {
-                    Type = type,
-                    Path = path,
-                    Process = process,
-                    ActiveRequestCount = 1,
-                    DeviceId = deviceId,
-                    CancellationTokenSource = cancellationTokenSource,
-                    Id = transcodingJobId,
-                    PlaySessionId = playSessionId,
-                    LiveStreamId = liveStreamId,
-                    MediaSource = state.MediaSource
-                };
-
-                _activeTranscodingJobs.Add(job);
-
-                ReportTranscodingProgress(job, state, null, null, null, null, null);
-
-                return job;
-            }
-        }
-
-        public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
-        {
-            var ticks = transcodingPosition?.Ticks;
-
-            if (job != null)
-            {
-                job.Framerate = framerate;
-                job.CompletionPercentage = percentComplete;
-                job.TranscodingPositionTicks = ticks;
-                job.BytesTranscoded = bytesTranscoded;
-                job.BitRate = bitRate;
-            }
-
-            var deviceId = state.Request.DeviceId;
-
-            if (!string.IsNullOrWhiteSpace(deviceId))
-            {
-                var audioCodec = state.ActualOutputAudioCodec;
-                var videoCodec = state.ActualOutputVideoCodec;
-
-                _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
-                {
-                    Bitrate = bitRate ?? state.TotalOutputBitrate,
-                    AudioCodec = audioCodec,
-                    VideoCodec = videoCodec,
-                    Container = state.OutputContainer,
-                    Framerate = framerate,
-                    CompletionPercentage = percentComplete,
-                    Width = state.OutputWidth,
-                    Height = state.OutputHeight,
-                    AudioChannels = state.OutputAudioChannels,
-                    IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
-                    IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
-                    TranscodeReasons = state.TranscodeReasons
-                });
-            }
-        }
-
-        /// <summary>
-        /// <summary>
-        /// The progressive.
-        /// </summary>
-        /// Called when [transcode failed to start].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="state">The state.</param>
-        public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
-        {
-            lock (_activeTranscodingJobs)
-            {
-                var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
-                if (job != null)
-                {
-                    _activeTranscodingJobs.Remove(job);
-                }
-            }
-
-            lock (_transcodingLocks)
-            {
-                _transcodingLocks.Remove(path);
-            }
-
-            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
-            {
-                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
-            }
-        }
-
-        /// <summary>
-        /// Determines whether [has active transcoding job] [the specified path].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="type">The type.</param>
-        /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
-        public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
-        {
-            return GetTranscodingJob(path, type) != null;
-        }
-
-        public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
-        {
-            lock (_activeTranscodingJobs)
-            {
-                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-            }
-        }
-
-        public TranscodingJob GetTranscodingJob(string playSessionId)
-        {
-            lock (_activeTranscodingJobs)
-            {
-                return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
-            }
-        }
-
-        /// <summary>
-        /// Called when [transcode begin request].
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="type">The type.</param>
-        public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
-        {
-            lock (_activeTranscodingJobs)
-            {
-                var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
-
-                if (job == null)
-                {
-                    return null;
-                }
-
-                OnTranscodeBeginRequest(job);
-
-                return job;
-            }
-        }
-
-        public void OnTranscodeBeginRequest(TranscodingJob job)
-        {
-            job.ActiveRequestCount++;
-
-            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
-            {
-                job.StopKillTimer();
-            }
-        }
-
-        public void OnTranscodeEndRequest(TranscodingJob job)
-        {
-            job.ActiveRequestCount--;
-            _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
-            if (job.ActiveRequestCount <= 0)
-            {
-                PingTimer(job, false);
-            }
-        }
-
-        internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
-        {
-            if (string.IsNullOrEmpty(playSessionId))
-            {
-                throw new ArgumentNullException(nameof(playSessionId));
-            }
-
-            _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
-
-            List<TranscodingJob> jobs;
-
-            lock (_activeTranscodingJobs)
-            {
-                // This is really only needed for HLS.
-                // Progressive streams can stop on their own reliably
-                jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
-            }
-
-            foreach (var job in jobs)
-            {
-                if (isUserPaused.HasValue)
-                {
-                    _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
-                    job.IsUserPaused = isUserPaused.Value;
-                }
-
-                PingTimer(job, true);
-            }
-        }
-
-        private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
-        {
-            if (job.HasExited)
-            {
-                job.StopKillTimer();
-                return;
-            }
-
-            var timerDuration = 10000;
-
-            if (job.Type != TranscodingJobType.Progressive)
-            {
-                timerDuration = 60000;
-            }
-
-            job.PingTimeout = timerDuration;
-            job.LastPingDate = DateTime.UtcNow;
-
-            // Don't start the timer for playback checkins with progressive streaming
-            if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
-            {
-                job.StartKillTimer(OnTranscodeKillTimerStopped);
-            }
-            else
-            {
-                job.ChangeKillTimerIfStarted();
-            }
-        }
-
-        /// <summary>
-        /// Called when [transcode kill timer stopped].
-        /// </summary>
-        /// <param name="state">The state.</param>
-        private async void OnTranscodeKillTimerStopped(object state)
-        {
-            var job = (TranscodingJob)state;
-
-            if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
-            {
-                var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
-
-                if (timeSinceLastPing < job.PingTimeout)
-                {
-                    job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
-                    return;
-                }
-            }
-
-            _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
-            await KillTranscodingJob(job, true, path => true);
-        }
-
-        /// <summary>
-        /// Kills the single transcoding job.
-        /// </summary>
-        /// <param name="deviceId">The device id.</param>
-        /// <param name="playSessionId">The play session identifier.</param>
-        /// <param name="deleteFiles">The delete files.</param>
-        /// <returns>Task.</returns>
-        internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
-        {
-            return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId)
-                ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
-                : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles);
-        }
-
-        /// <summary>
-        /// Kills the transcoding jobs.
-        /// </summary>
-        /// <param name="killJob">The kill job.</param>
-        /// <param name="deleteFiles">The delete files.</param>
-        /// <returns>Task.</returns>
-        private Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
-        {
-            var jobs = new List<TranscodingJob>();
-
-            lock (_activeTranscodingJobs)
-            {
-                // This is really only needed for HLS.
-                // Progressive streams can stop on their own reliably
-                jobs.AddRange(_activeTranscodingJobs.Where(killJob));
-            }
-
-            if (jobs.Count == 0)
-            {
-                return Task.CompletedTask;
-            }
-
-            IEnumerable<Task> GetKillJobs()
-            {
-                foreach (var job in jobs)
-                {
-                    yield return KillTranscodingJob(job, false, deleteFiles);
-                }
-            }
-
-            return Task.WhenAll(GetKillJobs());
-        }
-
-        /// <summary>
-        /// Kills the transcoding job.
-        /// </summary>
-        /// <param name="job">The job.</param>
-        /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
-        /// <param name="delete">The delete.</param>
-        private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
-        {
-            job.DisposeKillTimer();
-
-            _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
-
-            lock (_activeTranscodingJobs)
-            {
-                _activeTranscodingJobs.Remove(job);
-
-                if (!job.CancellationTokenSource.IsCancellationRequested)
-                {
-                    job.CancellationTokenSource.Cancel();
-                }
-            }
-
-            lock (_transcodingLocks)
-            {
-                _transcodingLocks.Remove(job.Path);
-            }
-
-            lock (job.ProcessLock)
-            {
-                job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
-
-                var process = job.Process;
-
-                var hasExited = job.HasExited;
-
-                if (!hasExited)
-                {
-                    try
-                    {
-                        _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
-
-                        process.StandardInput.WriteLine("q");
-
-                        // Need to wait because killing is asynchronous
-                        if (!process.WaitForExit(5000))
-                        {
-                            _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
-                            process.Kill();
-                        }
-                    }
-                    catch (InvalidOperationException)
-                    {
-                    }
-                }
-            }
-
-            if (delete(job.Path))
-            {
-                await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
-            }
-
-            if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
-            {
-                try
-                {
-                    await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
-                }
-            }
-        }
-
-        private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
-        {
-            if (retryCount >= 10)
-            {
-                return;
-            }
-
-            _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
-
-            await Task.Delay(delayMs).ConfigureAwait(false);
-
-            try
-            {
-                if (jobType == TranscodingJobType.Progressive)
-                {
-                    DeleteProgressivePartialStreamFiles(path);
-                }
-                else
-                {
-                    DeleteHlsPartialStreamFiles(path);
-                }
-            }
-            catch (IOException ex)
-            {
-                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
-
-                await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the progressive partial stream files.
-        /// </summary>
-        /// <param name="outputFilePath">The output file path.</param>
-        private void DeleteProgressivePartialStreamFiles(string outputFilePath)
-        {
-            if (File.Exists(outputFilePath))
-            {
-                _fileSystem.DeleteFile(outputFilePath);
-            }
-        }
-
-        /// <summary>
-        /// Deletes the HLS partial stream files.
-        /// </summary>
-        /// <param name="outputFilePath">The output file path.</param>
-        private void DeleteHlsPartialStreamFiles(string outputFilePath)
-        {
-            var directory = Path.GetDirectoryName(outputFilePath);
-            var name = Path.GetFileNameWithoutExtension(outputFilePath);
-
-            var filesToDelete = _fileSystem.GetFilePaths(directory)
-                .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
-
-            List<Exception> exs = null;
-            foreach (var file in filesToDelete)
-            {
-                try
-                {
-                    _logger.LogDebug("Deleting HLS file {0}", file);
-                    _fileSystem.DeleteFile(file);
-                }
-                catch (IOException ex)
-                {
-                    (exs ??= new List<Exception>(4)).Add(ex);
-                    _logger.LogError(ex, "Error deleting HLS file {Path}", file);
-                }
-            }
-
-            if (exs != null)
-            {
-                throw new AggregateException("Error deleting HLS files", exs);
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
deleted file mode 100644
index 63a31a7452..0000000000
--- a/MediaBrowser.Api/BaseApiService.cs
+++ /dev/null
@@ -1,416 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class BaseApiService.
-    /// </summary>
-    public abstract class BaseApiService : IService, IRequiresRequest
-    {
-        public BaseApiService(
-            ILogger<BaseApiService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory)
-        {
-            Logger = logger;
-            ServerConfigurationManager = serverConfigurationManager;
-            ResultFactory = httpResultFactory;
-        }
-
-        /// <summary>
-        /// Gets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        protected ILogger<BaseApiService> Logger { get; }
-
-        /// <summary>
-        /// Gets or sets the server configuration manager.
-        /// </summary>
-        /// <value>The server configuration manager.</value>
-        protected IServerConfigurationManager ServerConfigurationManager { get; }
-
-        /// <summary>
-        /// Gets the HTTP result factory.
-        /// </summary>
-        /// <value>The HTTP result factory.</value>
-        protected IHttpResultFactory ResultFactory { get; }
-
-        /// <summary>
-        /// Gets or sets the request context.
-        /// </summary>
-        /// <value>The request context.</value>
-        public IRequest Request { get; set; }
-
-        public string GetHeader(string name) => Request.Headers[name];
-
-        public static string[] SplitValue(string value, char delim)
-        {
-            return value == null
-                ? Array.Empty<string>()
-                : value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        public static Guid[] GetGuids(string value)
-        {
-            if (value == null)
-            {
-                return Array.Empty<Guid>();
-            }
-
-            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                        .Select(i => new Guid(i))
-                        .ToArray();
-        }
-
-        /// <summary>
-        /// To the optimized result.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="result">The result.</param>
-        /// <returns>System.Object.</returns>
-        protected object ToOptimizedResult<T>(T result)
-            where T : class
-        {
-            return ResultFactory.GetResult(Request, result);
-        }
-
-        protected void AssertCanUpdateUser(IAuthorizationContext authContext, IUserManager userManager, Guid userId, bool restrictUserPreferences)
-        {
-            var auth = authContext.GetAuthorizationInfo(Request);
-
-            var authenticatedUser = auth.User;
-
-            // If they're going to update the record of another user, they must be an administrator
-            if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
-                || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
-            {
-                throw new SecurityException("Unauthorized access.");
-            }
-        }
-
-        /// <summary>
-        /// Gets the session.
-        /// </summary>
-        /// <returns>SessionInfo.</returns>
-        protected SessionInfo GetSession(ISessionContext sessionContext)
-        {
-            var session = sessionContext.GetSession(Request);
-
-            if (session == null)
-            {
-                throw new ArgumentException("Session not found.");
-            }
-
-            return session;
-        }
-
-        protected DtoOptions GetDtoOptions(IAuthorizationContext authContext, object request)
-        {
-            var options = new DtoOptions();
-
-            if (request is IHasItemFields hasFields)
-            {
-                options.Fields = hasFields.GetItemFields();
-            }
-
-            if (!options.ContainsField(ItemFields.RecursiveItemCount)
-                || !options.ContainsField(ItemFields.ChildCount))
-            {
-                var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
-                if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
-                    client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
-                    client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
-                    client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    int oldLen = options.Fields.Length;
-                    var arr = new ItemFields[oldLen + 1];
-                    options.Fields.CopyTo(arr, 0);
-                    arr[oldLen] = ItemFields.RecursiveItemCount;
-                    options.Fields = arr;
-                }
-
-                if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
-                   client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-
-                    int oldLen = options.Fields.Length;
-                    var arr = new ItemFields[oldLen + 1];
-                    options.Fields.CopyTo(arr, 0);
-                    arr[oldLen] = ItemFields.ChildCount;
-                    options.Fields = arr;
-                }
-            }
-
-            if (request is IHasDtoOptions hasDtoOptions)
-            {
-                options.EnableImages = hasDtoOptions.EnableImages ?? true;
-
-                if (hasDtoOptions.ImageTypeLimit.HasValue)
-                {
-                    options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
-                }
-
-                if (hasDtoOptions.EnableUserData.HasValue)
-                {
-                    options.EnableUserData = hasDtoOptions.EnableUserData.Value;
-                }
-
-                if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
-                {
-                    options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
-                                                                        .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
-                                                                        .ToArray();
-                }
-            }
-
-            return options;
-        }
-
-        protected MusicArtist GetArtist(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (name.IndexOf(BaseItem.SlugChar) != -1)
-            {
-                var result = GetItemFromSlugName<MusicArtist>(libraryManager, name, dtoOptions);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            return libraryManager.GetArtist(name, dtoOptions);
-        }
-
-        protected Studio GetStudio(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (name.IndexOf(BaseItem.SlugChar) != -1)
-            {
-                var result = GetItemFromSlugName<Studio>(libraryManager, name, dtoOptions);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            return libraryManager.GetStudio(name);
-        }
-
-        protected Genre GetGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (name.IndexOf(BaseItem.SlugChar) != -1)
-            {
-                var result = GetItemFromSlugName<Genre>(libraryManager, name, dtoOptions);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            return libraryManager.GetGenre(name);
-        }
-
-        protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (name.IndexOf(BaseItem.SlugChar) != -1)
-            {
-                var result = GetItemFromSlugName<MusicGenre>(libraryManager, name, dtoOptions);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            return libraryManager.GetMusicGenre(name);
-        }
-
-        protected Person GetPerson(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (name.IndexOf(BaseItem.SlugChar) != -1)
-            {
-                var result = GetItemFromSlugName<Person>(libraryManager, name, dtoOptions);
-
-                if (result != null)
-                {
-                    return result;
-                }
-            }
-
-            return libraryManager.GetPerson(name);
-        }
-
-        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
-            where T : BaseItem, new()
-        {
-            var result = libraryManager.GetItemList(new InternalItemsQuery
-            {
-                Name = name.Replace(BaseItem.SlugChar, '&'),
-                IncludeItemTypes = new[] { typeof(T).Name },
-                DtoOptions = dtoOptions
-            }).OfType<T>().FirstOrDefault();
-
-            result ??= libraryManager.GetItemList(new InternalItemsQuery
-            {
-                Name = name.Replace(BaseItem.SlugChar, '/'),
-                IncludeItemTypes = new[] { typeof(T).Name },
-                DtoOptions = dtoOptions
-            }).OfType<T>().FirstOrDefault();
-
-            result ??= libraryManager.GetItemList(new InternalItemsQuery
-            {
-                Name = name.Replace(BaseItem.SlugChar, '?'),
-                IncludeItemTypes = new[] { typeof(T).Name },
-                DtoOptions = dtoOptions
-            }).OfType<T>().FirstOrDefault();
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the path segment at the specified index.
-        /// </summary>
-        /// <param name="index">The index of the path segment.</param>
-        /// <returns>The path segment at the specified index.</returns>
-        /// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
-        /// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
-        protected internal ReadOnlySpan<char> GetPathValue(int index)
-        {
-            static void ThrowIndexOutOfRangeException()
-                => throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
-
-            static void ThrowInvalidDataException()
-                => throw new InvalidDataException("Path doesn't start with the base url.");
-
-            ReadOnlySpan<char> path = Request.PathInfo;
-
-            // Remove the protocol part from the url
-            int pos = path.LastIndexOf("://");
-            if (pos != -1)
-            {
-                path = path.Slice(pos + 3);
-            }
-
-            // Remove the query string
-            pos = path.LastIndexOf('?');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            // Remove the domain
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(pos);
-            }
-
-            // Remove base url
-            string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
-            int baseUrlLen = baseUrl.Length;
-            if (baseUrlLen != 0)
-            {
-                if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
-                {
-                    path = path.Slice(baseUrlLen);
-                }
-                else
-                {
-                    // The path doesn't start with the base url,
-                    // how did we get here?
-                    ThrowInvalidDataException();
-                }
-            }
-
-            // Remove leading /
-            path = path.Slice(1);
-
-            // Backwards compatibility
-            const string Emby = "emby/";
-            if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(Emby.Length);
-            }
-
-            const string MediaBrowser = "mediabrowser/";
-            if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
-            {
-                path = path.Slice(MediaBrowser.Length);
-            }
-
-            // Skip segments until we are at the right index
-            for (int i = 0; i < index; i++)
-            {
-                pos = path.IndexOf('/');
-                if (pos == -1)
-                {
-                    ThrowIndexOutOfRangeException();
-                }
-
-                path = path.Slice(pos + 1);
-            }
-
-            // Remove the rest
-            pos = path.IndexOf('/');
-            if (pos != -1)
-            {
-                path = path.Slice(0, pos);
-            }
-
-            return path;
-        }
-
-        /// <summary>
-        /// Gets the name of the item by.
-        /// </summary>
-        protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager, DtoOptions dtoOptions)
-        {
-            if (type.Equals("Person", StringComparison.OrdinalIgnoreCase))
-            {
-                return GetPerson(name, libraryManager, dtoOptions);
-            }
-            else if (type.Equals("Artist", StringComparison.OrdinalIgnoreCase))
-            {
-                return GetArtist(name, libraryManager, dtoOptions);
-            }
-            else if (type.Equals("Genre", StringComparison.OrdinalIgnoreCase))
-            {
-                return GetGenre(name, libraryManager, dtoOptions);
-            }
-            else if (type.Equals("MusicGenre", StringComparison.OrdinalIgnoreCase))
-            {
-                return GetMusicGenre(name, libraryManager, dtoOptions);
-            }
-            else if (type.Equals("Studio", StringComparison.OrdinalIgnoreCase))
-            {
-                return GetStudio(name, libraryManager, dtoOptions);
-            }
-            else if (type.Equals("Year", StringComparison.OrdinalIgnoreCase))
-            {
-                return libraryManager.GetYear(int.Parse(name));
-            }
-
-            throw new ArgumentException("Invalid type", nameof(type));
-        }
-    }
-}
diff --git a/MediaBrowser.Api/IHasDtoOptions.cs b/MediaBrowser.Api/IHasDtoOptions.cs
deleted file mode 100644
index 33d498e8bd..0000000000
--- a/MediaBrowser.Api/IHasDtoOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace MediaBrowser.Api
-{
-    public interface IHasDtoOptions : IHasItemFields
-    {
-        bool? EnableImages { get; set; }
-
-        bool? EnableUserData { get; set; }
-
-        int? ImageTypeLimit { get; set; }
-
-        string EnableImageTypes { get; set; }
-    }
-}
diff --git a/MediaBrowser.Api/IHasItemFields.cs b/MediaBrowser.Api/IHasItemFields.cs
deleted file mode 100644
index ad4f1b4891..0000000000
--- a/MediaBrowser.Api/IHasItemFields.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.Linq;
-using MediaBrowser.Model.Querying;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Interface IHasItemFields.
-    /// </summary>
-    public interface IHasItemFields
-    {
-        /// <summary>
-        /// Gets or sets the fields.
-        /// </summary>
-        /// <value>The fields.</value>
-        string Fields { get; set; }
-    }
-
-    /// <summary>
-    /// Class ItemFieldsExtensions.
-    /// </summary>
-    public static class ItemFieldsExtensions
-    {
-        /// <summary>
-        /// Gets the item fields.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        public static ItemFields[] GetItemFields(this IHasItemFields request)
-        {
-            var val = request.Fields;
-
-            if (string.IsNullOrEmpty(val))
-            {
-                return Array.Empty<ItemFields>();
-            }
-
-            return val.Split(',').Select(v =>
-            {
-                if (Enum.TryParse(v, true, out ItemFields value))
-                {
-                    return (ItemFields?)value;
-                }
-
-                return null;
-            }).Where(i => i.HasValue).Select(i => i.Value).ToArray();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
deleted file mode 100644
index 6773bf2808..0000000000
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ /dev/null
@@ -1,29 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
-  <PropertyGroup>
-    <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Compile Include="..\SharedVersion.cs" />
-    <Compile Remove="Images\ImageService.cs" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Folder Include="Sessions" />
-    <Folder Include="System" />
-  </ItemGroup>
-
-  <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
-    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
-    <GenerateDocumentationFile>true</GenerateDocumentationFile>
-  </PropertyGroup>
-
-</Project>
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
deleted file mode 100644
index 84ed5dcac4..0000000000
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ /dev/null
@@ -1,1008 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
-    /// <summary>
-    /// Class BaseStreamingService.
-    /// </summary>
-    public abstract class BaseStreamingService : BaseApiService
-    {
-        protected virtual bool EnableOutputInSubFolder => false;
-
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        protected IUserManager UserManager { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the library manager.
-        /// </summary>
-        /// <value>The library manager.</value>
-        protected ILibraryManager LibraryManager { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the iso manager.
-        /// </summary>
-        /// <value>The iso manager.</value>
-        protected IIsoManager IsoManager { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the media encoder.
-        /// </summary>
-        /// <value>The media encoder.</value>
-        protected IMediaEncoder MediaEncoder { get; private set; }
-
-        protected IFileSystem FileSystem { get; private set; }
-
-        protected IDlnaManager DlnaManager { get; private set; }
-
-        protected IDeviceManager DeviceManager { get; private set; }
-
-        protected IMediaSourceManager MediaSourceManager { get; private set; }
-
-        protected IJsonSerializer JsonSerializer { get; private set; }
-
-        protected IAuthorizationContext AuthorizationContext { get; private set; }
-
-        protected EncodingHelper EncodingHelper { get; set; }
-
-        /// <summary>
-        /// Gets the type of the transcoding job.
-        /// </summary>
-        /// <value>The type of the transcoding job.</value>
-        protected abstract TranscodingJobType TranscodingJobType { get; }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
-        /// </summary>
-        protected BaseStreamingService(
-            ILogger<BaseStreamingService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-            UserManager = userManager;
-            LibraryManager = libraryManager;
-            IsoManager = isoManager;
-            MediaEncoder = mediaEncoder;
-            FileSystem = fileSystem;
-            DlnaManager = dlnaManager;
-            DeviceManager = deviceManager;
-            MediaSourceManager = mediaSourceManager;
-            JsonSerializer = jsonSerializer;
-            AuthorizationContext = authorizationContext;
-
-            EncodingHelper = encodingHelper;
-        }
-
-        /// <summary>
-        /// Gets the command line arguments.
-        /// </summary>
-        protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding);
-
-        /// <summary>
-        /// Gets the output file extension.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected virtual string GetOutputFileExtension(StreamState state)
-        {
-            return Path.GetExtension(state.RequestedUrl);
-        }
-
-        /// <summary>
-        /// Gets the output file path.
-        /// </summary>
-        private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension)
-        {
-            var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}";
-
-            var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-            var ext = outputFileExtension?.ToLowerInvariant();
-            var folder = ServerConfigurationManager.GetTranscodePath();
-
-            return EnableOutputInSubFolder
-                ? Path.Combine(folder, filename, filename + ext)
-                : Path.Combine(folder, filename + ext);
-        }
-
-        protected virtual string GetDefaultEncoderPreset()
-        {
-            return "superfast";
-        }
-
-        private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
-        {
-            if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
-            {
-                state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
-
-            if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
-            {
-                var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
-                {
-                    OpenToken = state.MediaSource.OpenToken
-                }, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
-
-                if (state.VideoRequest != null)
-                {
-                    EncodingHelper.TryStreamCopy(state);
-                }
-            }
-
-            if (state.MediaSource.BufferMs.HasValue)
-            {
-                await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Starts the FFMPEG.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="outputPath">The output path.</param>
-        /// <param name="cancellationTokenSource">The cancellation token source.</param>
-        /// <param name="workingDirectory">The working directory.</param>
-        /// <returns>Task.</returns>
-        protected async Task<TranscodingJob> StartFfMpeg(
-            StreamState state,
-            string outputPath,
-            CancellationTokenSource cancellationTokenSource,
-            string workingDirectory = null)
-        {
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
-
-            await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
-
-            if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
-            {
-                var auth = AuthorizationContext.GetAuthorizationInfo(Request);
-                if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
-                {
-                    ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
-                    throw new ArgumentException("User does not have access to video transcoding");
-                }
-            }
-
-            var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
-            var process = new Process()
-            {
-                StartInfo = new ProcessStartInfo()
-                {
-                    WindowStyle = ProcessWindowStyle.Hidden,
-                    CreateNoWindow = true,
-                    UseShellExecute = false,
-
-                    // Must consume both stdout and stderr or deadlocks may occur
-                    // RedirectStandardOutput = true,
-                    RedirectStandardError = true,
-                    RedirectStandardInput = true,
-
-                    FileName = MediaEncoder.EncoderPath,
-                    Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true),
-                    WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
-
-                    ErrorDialog = false
-                },
-                EnableRaisingEvents = true
-            };
-
-            var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
-                state.Request.PlaySessionId,
-                state.MediaSource.LiveStreamId,
-                Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
-                TranscodingJobType,
-                process,
-                state.Request.DeviceId,
-                state,
-                cancellationTokenSource);
-
-            var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
-            Logger.LogInformation(commandLineLogMessage);
-
-            var logFilePrefix = "ffmpeg-transcode";
-            if (state.VideoRequest != null
-                && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
-            {
-                logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
-                    ? "ffmpeg-remux" : "ffmpeg-directstream";
-            }
-
-            var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
-
-            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
-            Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-
-            var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
-            await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
-
-            process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
-
-            try
-            {
-                process.Start();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error starting ffmpeg");
-
-                ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
-                throw;
-            }
-
-            Logger.LogDebug("Launched ffmpeg process");
-            state.TranscodingJob = transcodingJob;
-
-            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
-            _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
-
-            // Wait for the file to exist before proceeeding
-            var ffmpegTargetFile = state.WaitForPath ?? outputPath;
-            Logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
-            while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
-            {
-                await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
-            }
-
-            Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
-
-            if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
-            {
-                await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
-
-                if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
-                {
-                    await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
-                }
-            }
-
-            if (!transcodingJob.HasExited)
-            {
-                StartThrottler(state, transcodingJob);
-            }
-
-            Logger.LogDebug("StartFfMpeg() finished successfully");
-
-            return transcodingJob;
-        }
-
-        private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
-        {
-            if (EnableThrottling(state))
-            {
-                transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager, FileSystem);
-                state.TranscodingThrottler.Start();
-            }
-        }
-
-        private bool EnableThrottling(StreamState state)
-        {
-            var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
-            // enable throttling when NOT using hardware acceleration
-            if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
-            {
-                return state.InputProtocol == MediaProtocol.File &&
-                       state.RunTimeTicks.HasValue &&
-                       state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
-                       state.IsInputVideo &&
-                       state.VideoType == VideoType.VideoFile &&
-                       !EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Processes the exited.
-        /// </summary>
-        /// <param name="process">The process.</param>
-        /// <param name="job">The job.</param>
-        /// <param name="state">The state.</param>
-        private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
-        {
-            if (job != null)
-            {
-                job.HasExited = true;
-            }
-
-            Logger.LogDebug("Disposing stream resources");
-            state.Dispose();
-
-            if (process.ExitCode == 0)
-            {
-                Logger.LogInformation("FFMpeg exited with code 0");
-            }
-            else
-            {
-                Logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
-            }
-
-            process.Dispose();
-        }
-
-        /// <summary>
-        /// Parses the parameters.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        private void ParseParams(StreamRequest request)
-        {
-            var vals = request.Params.Split(';');
-
-            var videoRequest = request as VideoStreamRequest;
-
-            for (var i = 0; i < vals.Length; i++)
-            {
-                var val = vals[i];
-
-                if (string.IsNullOrWhiteSpace(val))
-                {
-                    continue;
-                }
-
-                switch (i)
-                {
-                    case 0:
-                        request.DeviceProfileId = val;
-                        break;
-                    case 1:
-                        request.DeviceId = val;
-                        break;
-                    case 2:
-                        request.MediaSourceId = val;
-                        break;
-                    case 3:
-                        request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        break;
-                    case 4:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.VideoCodec = val;
-                        }
-
-                        break;
-                    case 5:
-                        request.AudioCodec = val;
-                        break;
-                    case 6:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 7:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 8:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 9:
-                        request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
-                        break;
-                    case 10:
-                        request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
-                        break;
-                    case 11:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 12:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 13:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 14:
-                        request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
-                        break;
-                    case 15:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.Level = val;
-                        }
-
-                        break;
-                    case 16:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 17:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
-                        }
-
-                        break;
-                    case 18:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.Profile = val;
-                        }
-
-                        break;
-                    case 19:
-                        // cabac no longer used
-                        break;
-                    case 20:
-                        request.PlaySessionId = val;
-                        break;
-                    case 21:
-                        // api_key
-                        break;
-                    case 22:
-                        request.LiveStreamId = val;
-                        break;
-                    case 23:
-                        // Duplicating ItemId because of MediaMonkey
-                        break;
-                    case 24:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        }
-
-                        break;
-                    case 25:
-                        if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
-                        {
-                            if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
-                            {
-                                videoRequest.SubtitleMethod = method;
-                            }
-                        }
-
-                        break;
-                    case 26:
-                        request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
-                        break;
-                    case 27:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        }
-
-                        break;
-                    case 28:
-                        request.Tag = val;
-                        break;
-                    case 29:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        }
-
-                        break;
-                    case 30:
-                        request.SubtitleCodec = val;
-                        break;
-                    case 31:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        }
-
-                        break;
-                    case 32:
-                        if (videoRequest != null)
-                        {
-                            videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                        }
-
-                        break;
-                    case 33:
-                        request.TranscodeReasons = val;
-                        break;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Parses query parameters as StreamOptions.
-        /// </summary>
-        /// <param name="request">The stream request.</param>
-        private void ParseStreamOptions(StreamRequest request)
-        {
-            foreach (var param in Request.QueryString)
-            {
-                if (char.IsLower(param.Key[0]))
-                {
-                    // This was probably not parsed initially and should be a StreamOptions
-                    // TODO: This should be incorporated either in the lower framework for parsing requests
-                    // or the generated URL should correctly serialize it
-                    request.StreamOptions[param.Key] = param.Value;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Parses the dlna headers.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        private void ParseDlnaHeaders(StreamRequest request)
-        {
-            if (!request.StartTimeTicks.HasValue)
-            {
-                var timeSeek = GetHeader("TimeSeekRange.dlna.org");
-
-                request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
-            }
-        }
-
-        /// <summary>
-        /// Parses the time seek header.
-        /// </summary>
-        private long? ParseTimeSeekHeader(string value)
-        {
-            if (string.IsNullOrWhiteSpace(value))
-            {
-                return null;
-            }
-
-            const string Npt = "npt=";
-            if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
-            {
-                throw new ArgumentException("Invalid timeseek header");
-            }
-
-            int index = value.IndexOf('-');
-            value = index == -1
-                ? value.Substring(Npt.Length)
-                : value.Substring(Npt.Length, index - Npt.Length);
-
-            if (value.IndexOf(':') == -1)
-            {
-                // Parses npt times in the format of '417.33'
-                if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
-                {
-                    return TimeSpan.FromSeconds(seconds).Ticks;
-                }
-
-                throw new ArgumentException("Invalid timeseek header");
-            }
-
-            // Parses npt times in the format of '10:19:25.7'
-            var tokens = value.Split(new[] { ':' }, 3);
-            double secondsSum = 0;
-            var timeFactor = 3600;
-
-            foreach (var time in tokens)
-            {
-                if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
-                {
-                    secondsSum += digit * timeFactor;
-                }
-                else
-                {
-                    throw new ArgumentException("Invalid timeseek header");
-                }
-
-                timeFactor /= 60;
-            }
-
-            return TimeSpan.FromSeconds(secondsSum).Ticks;
-        }
-
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>StreamState.</returns>
-        protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
-        {
-            ParseDlnaHeaders(request);
-
-            if (!string.IsNullOrWhiteSpace(request.Params))
-            {
-                ParseParams(request);
-            }
-
-            ParseStreamOptions(request);
-
-            var url = Request.PathInfo;
-
-            if (string.IsNullOrEmpty(request.AudioCodec))
-            {
-                request.AudioCodec = EncodingHelper.InferAudioCodec(url);
-            }
-
-            var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) ||
-                                    string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase);
-
-            var state = new StreamState(MediaSourceManager, TranscodingJobType)
-            {
-                Request = request,
-                RequestedUrl = url,
-                UserAgent = Request.UserAgent,
-                EnableDlnaHeaders = enableDlnaHeaders
-            };
-
-            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
-            if (!auth.UserId.Equals(Guid.Empty))
-            {
-                state.User = UserManager.GetUserById(auth.UserId);
-            }
-
-            // if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
-            //    (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
-            //    (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
-            //{
-            //    state.SegmentLength = 6;
-            //}
-
-            if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
-            {
-                state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.AudioCodec))
-            {
-                state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
-                    ?? state.SupportedAudioCodecs.FirstOrDefault();
-            }
-
-            if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
-            {
-                state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-                state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToSubtitleCodec(i))
-                    ?? state.SupportedSubtitleCodecs.FirstOrDefault();
-            }
-
-            var item = LibraryManager.GetItemById(request.Id);
-
-            state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
-            // var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
-            //             item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
-            // if (primaryImage != null)
-            //{
-            //    state.AlbumCoverPath = primaryImage.Path;
-            //}
-
-            MediaSourceInfo mediaSource = null;
-            if (string.IsNullOrWhiteSpace(request.LiveStreamId))
-            {
-                var currentJob = !string.IsNullOrWhiteSpace(request.PlaySessionId) ?
-                    ApiEntryPoint.Instance.GetTranscodingJob(request.PlaySessionId)
-                    : null;
-
-                if (currentJob != null)
-                {
-                    mediaSource = currentJob.MediaSource;
-                }
-
-                if (mediaSource == null)
-                {
-                    var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
-
-                    mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
-                       ? mediaSources[0]
-                       : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId));
-
-                    if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id)
-                    {
-                        mediaSource = mediaSources[0];
-                    }
-                }
-            }
-            else
-            {
-                var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
-                mediaSource = liveStreamInfo.Item1;
-                state.DirectStreamProvider = liveStreamInfo.Item2;
-            }
-
-            var videoRequest = request as VideoStreamRequest;
-
-            EncodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
-
-            var container = Path.GetExtension(state.RequestedUrl);
-
-            if (string.IsNullOrEmpty(container))
-            {
-                container = request.Container;
-            }
-
-            if (string.IsNullOrEmpty(container))
-            {
-                container = request.Static ?
-                    StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) :
-                    GetOutputFileExtension(state);
-            }
-
-            state.OutputContainer = (container ?? string.Empty).TrimStart('.');
-
-            state.OutputAudioBitrate = EncodingHelper.GetAudioBitrateParam(state.Request, state.AudioStream);
-
-            state.OutputAudioCodec = state.Request.AudioCodec;
-
-            state.OutputAudioChannels = EncodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
-
-            if (videoRequest != null)
-            {
-                state.OutputVideoCodec = state.VideoRequest.VideoCodec;
-                state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
-
-                if (videoRequest != null)
-                {
-                    EncodingHelper.TryStreamCopy(state);
-                }
-
-                if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
-                {
-                    var resolution = ResolutionNormalizer.Normalize(
-                        state.VideoStream?.BitRate,
-                        state.VideoStream?.Width,
-                        state.VideoStream?.Height,
-                        state.OutputVideoBitrate.Value,
-                        state.VideoStream?.Codec,
-                        state.OutputVideoCodec,
-                        videoRequest.MaxWidth,
-                        videoRequest.MaxHeight);
-
-                    videoRequest.MaxWidth = resolution.MaxWidth;
-                    videoRequest.MaxHeight = resolution.MaxHeight;
-                }
-            }
-
-            ApplyDeviceProfileSettings(state);
-
-            var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
-                ? GetOutputFileExtension(state)
-                : ('.' + state.OutputContainer);
-
-            var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
-            state.OutputFilePath = GetOutputFilePath(state, encodingOptions, ext);
-
-            return state;
-        }
-
-        private void ApplyDeviceProfileSettings(StreamState state)
-        {
-            var headers = Request.Headers;
-
-            if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
-            {
-                state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
-            }
-            else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
-            {
-                var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
-
-                state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile;
-            }
-
-            var profile = state.DeviceProfile;
-
-            if (profile == null)
-            {
-                // Don't use settings from the default profile.
-                // Only use a specific profile if it was requested.
-                return;
-            }
-
-            var audioCodec = state.ActualOutputAudioCodec;
-            var videoCodec = state.ActualOutputVideoCodec;
-
-            var mediaProfile = state.VideoRequest == null ?
-                profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) :
-                profile.GetVideoMediaProfile(state.OutputContainer,
-                audioCodec,
-                videoCodec,
-                state.OutputWidth,
-                state.OutputHeight,
-                state.TargetVideoBitDepth,
-                state.OutputVideoBitrate,
-                state.TargetVideoProfile,
-                state.TargetVideoLevel,
-                state.TargetFramerate,
-                state.TargetPacketLength,
-                state.TargetTimestamp,
-                state.IsTargetAnamorphic,
-                state.IsTargetInterlaced,
-                state.TargetRefFrames,
-                state.TargetVideoStreamCount,
-                state.TargetAudioStreamCount,
-                state.TargetVideoCodecTag,
-                state.IsTargetAVC);
-
-            if (mediaProfile != null)
-            {
-                state.MimeType = mediaProfile.MimeType;
-            }
-
-            if (!state.Request.Static)
-            {
-                var transcodingProfile = state.VideoRequest == null ?
-                    profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
-                    profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
-
-                if (transcodingProfile != null)
-                {
-                    state.EstimateContentLength = transcodingProfile.EstimateContentLength;
-                    // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
-                    state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
-                    if (state.VideoRequest != null)
-                    {
-                        state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
-                        state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Adds the dlna headers.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
-        {
-            if (!state.EnableDlnaHeaders)
-            {
-                return;
-            }
-
-            var profile = state.DeviceProfile;
-
-            var transferMode = GetHeader("transferMode.dlna.org");
-            responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
-            responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
-
-            if (state.RunTimeTicks.HasValue)
-            {
-                if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
-                {
-                    var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
-                    responseHeaders["MediaInfo.sec"] = string.Format(
-                        CultureInfo.InvariantCulture,
-                        "SEC_Duration={0};",
-                        Convert.ToInt32(ms));
-                }
-
-                if (!isStaticallyStreamed && profile != null)
-                {
-                    AddTimeSeekResponseHeaders(state, responseHeaders);
-                }
-            }
-
-            if (profile == null)
-            {
-                profile = DlnaManager.GetDefaultProfile();
-            }
-
-            var audioCodec = state.ActualOutputAudioCodec;
-
-            if (state.VideoRequest == null)
-            {
-                responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader(
-                    state.OutputContainer,
-                    audioCodec,
-                    state.OutputAudioBitrate,
-                    state.OutputAudioSampleRate,
-                    state.OutputAudioChannels,
-                    state.OutputAudioBitDepth,
-                    isStaticallyStreamed,
-                    state.RunTimeTicks,
-                    state.TranscodeSeekInfo);
-            }
-            else
-            {
-                var videoCodec = state.ActualOutputVideoCodec;
-
-                responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader(
-                    state.OutputContainer,
-                    videoCodec,
-                    audioCodec,
-                    state.OutputWidth,
-                    state.OutputHeight,
-                    state.TargetVideoBitDepth,
-                    state.OutputVideoBitrate,
-                    state.TargetTimestamp,
-                    isStaticallyStreamed,
-                    state.RunTimeTicks,
-                    state.TargetVideoProfile,
-                    state.TargetVideoLevel,
-                    state.TargetFramerate,
-                    state.TargetPacketLength,
-                    state.TranscodeSeekInfo,
-                    state.IsTargetAnamorphic,
-                    state.IsTargetInterlaced,
-                    state.TargetRefFrames,
-                    state.TargetVideoStreamCount,
-                    state.TargetAudioStreamCount,
-                    state.TargetVideoCodecTag,
-                    state.IsTargetAVC).FirstOrDefault() ?? string.Empty;
-            }
-        }
-
-        private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
-        {
-            var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-            var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-
-            responseHeaders["TimeSeekRange.dlna.org"] = string.Format(
-                CultureInfo.InvariantCulture,
-                "npt={0}-{1}/{1}",
-                startSeconds,
-                runtimeSeconds);
-            responseHeaders["X-AvailableSeekRange"] = string.Format(
-                CultureInfo.InvariantCulture,
-                "1 npt={0}-{1}",
-                startSeconds,
-                runtimeSeconds);
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
deleted file mode 100644
index c80e8e64f7..0000000000
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ /dev/null
@@ -1,344 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
-    /// <summary>
-    /// Class BaseHlsService.
-    /// </summary>
-    public abstract class BaseHlsService : BaseStreamingService
-    {
-        public BaseHlsService(
-            ILogger<BaseHlsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-        }
-
-        /// <summary>
-        /// Gets the audio arguments.
-        /// </summary>
-        protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
-
-        /// <summary>
-        /// Gets the video arguments.
-        /// </summary>
-        protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
-
-        /// <summary>
-        /// Gets the segment file extension.
-        /// </summary>
-        protected string GetSegmentFileExtension(StreamRequest request)
-        {
-            var segmentContainer = request.SegmentContainer;
-            if (!string.IsNullOrWhiteSpace(segmentContainer))
-            {
-                return "." + segmentContainer;
-            }
-
-            return ".ts";
-        }
-
-        /// <summary>
-        /// Gets the type of the transcoding job.
-        /// </summary>
-        /// <value>The type of the transcoding job.</value>
-        protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Hls;
-
-        /// <summary>
-        /// Processes the request async.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="isLive">if set to <c>true</c> [is live].</param>
-        /// <returns>Task{System.Object}.</returns>
-        /// <exception cref="ArgumentException">A video bitrate is required
-        /// or
-        /// An audio bitrate is required</exception>
-        protected async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
-        {
-            var cancellationTokenSource = new CancellationTokenSource();
-
-            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
-
-            TranscodingJob job = null;
-            var playlist = state.OutputFilePath;
-
-            if (!File.Exists(playlist))
-            {
-                var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
-                await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
-                try
-                {
-                    if (!File.Exists(playlist))
-                    {
-                        // If the playlist doesn't already exist, startup ffmpeg
-                        try
-                        {
-                            job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
-                            job.IsLiveOutput = isLive;
-                        }
-                        catch
-                        {
-                            state.Dispose();
-                            throw;
-                        }
-
-                        var minSegments = state.MinSegments;
-                        if (minSegments > 0)
-                        {
-                            await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
-                        }
-                    }
-                }
-                finally
-                {
-                    transcodingLock.Release();
-                }
-            }
-
-            if (isLive)
-            {
-                job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
-
-                if (job != null)
-                {
-                    ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
-                }
-
-                return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-            }
-
-            var audioBitrate = state.OutputAudioBitrate ?? 0;
-            var videoBitrate = state.OutputVideoBitrate ?? 0;
-
-            var baselineStreamBitrate = 64000;
-
-            var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
-
-            job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
-
-            if (job != null)
-            {
-                ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
-            }
-
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-        }
-
-        private string GetLivePlaylistText(string path, int segmentLength)
-        {
-            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-            using var reader = new StreamReader(stream);
-
-            var text = reader.ReadToEnd();
-
-            text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
-
-            var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
-
-            text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
-            // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
-
-            return text;
-        }
-
-        private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
-        {
-            var builder = new StringBuilder();
-
-            builder.AppendLine("#EXTM3U");
-
-            // Pad a little to satisfy the apple hls validator
-            var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
-
-            // Main stream
-            builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
-                .AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
-            var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
-            builder.AppendLine(playlistUrl);
-
-            return builder.ToString();
-        }
-
-        protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
-        {
-            Logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
-
-            while (!cancellationToken.IsCancellationRequested)
-            {
-                try
-                {
-                    // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
-                    var fileStream = GetPlaylistFileStream(playlist);
-                    await using (fileStream.ConfigureAwait(false))
-                    {
-                        using var reader = new StreamReader(fileStream);
-                        var count = 0;
-
-                        while (!reader.EndOfStream)
-                        {
-                            var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
-                            if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
-                            {
-                                count++;
-                                if (count >= segmentCount)
-                                {
-                                    Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
-                                    return;
-                                }
-                            }
-                        }
-                    }
-
-                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                }
-                catch (IOException)
-                {
-                    // May get an error if the file is locked
-                }
-
-                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        protected Stream GetPlaylistFileStream(string path)
-        {
-            return new FileStream(
-                path,
-                FileMode.Open,
-                FileAccess.Read,
-                FileShare.ReadWrite,
-                IODefaults.FileStreamBufferSize,
-                FileOptions.SequentialScan);
-        }
-
-        protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
-        {
-            var itsOffsetMs = 0;
-
-            var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
-
-            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
-
-            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
-            // If isEncoding is true we're actually starting ffmpeg
-            var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
-
-            var baseUrlParam = string.Empty;
-
-            if (state.Request is GetLiveHlsStream)
-            {
-                baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
-                    "hls/" + Path.GetFileNameWithoutExtension(outputPath));
-            }
-
-            var useGenericSegmenter = true;
-            if (useGenericSegmenter)
-            {
-                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
-
-                var timeDeltaParam = string.Empty;
-
-                var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
-                if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
-                {
-                    segmentFormat = "mpegts";
-                }
-
-                baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
-
-                return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
-                    inputModifier,
-                    EncodingHelper.GetInputArgument(state, encodingOptions),
-                    threads,
-                    EncodingHelper.GetMapArgs(state),
-                    GetVideoArguments(state, encodingOptions),
-                    GetAudioArguments(state, encodingOptions),
-                    state.SegmentLength.ToString(CultureInfo.InvariantCulture),
-                    startNumberParam,
-                    outputPath,
-                    outputTsArg,
-                    timeDeltaParam,
-                    segmentFormat,
-                    baseUrlParam
-                ).Trim();
-            }
-
-            // add when stream copying?
-            // -avoid_negative_ts make_zero -fflags +genpts
-
-            var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
-                itsOffset,
-                inputModifier,
-                EncodingHelper.GetInputArgument(state, encodingOptions),
-                threads,
-                EncodingHelper.GetMapArgs(state),
-                GetVideoArguments(state, encodingOptions),
-                GetAudioArguments(state, encodingOptions),
-                state.SegmentLength.ToString(CultureInfo.InvariantCulture),
-                startNumberParam,
-                state.HlsListSize.ToString(CultureInfo.InvariantCulture),
-                baseUrlParam,
-                outputPath
-                ).Trim();
-
-            return args;
-        }
-
-        protected override string GetDefaultEncoderPreset()
-        {
-            return "veryfast";
-        }
-
-        protected virtual int GetStartNumber(StreamState state)
-        {
-            return 0;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
deleted file mode 100644
index 97ae0f0fde..0000000000
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ /dev/null
@@ -1,1226 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
-    /// <summary>
-    /// Options is needed for chromecast. Threw Head in there since it's related
-    /// </summary>
-    public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
-    {
-        public bool EnableAdaptiveBitrateStreaming { get; set; }
-
-        public GetMasterHlsVideoPlaylist()
-        {
-            EnableAdaptiveBitrateStreaming = true;
-        }
-    }
-
-    public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
-    {
-        public bool EnableAdaptiveBitrateStreaming { get; set; }
-
-        public GetMasterHlsAudioPlaylist()
-        {
-            EnableAdaptiveBitrateStreaming = true;
-        }
-    }
-
-    public interface IMasterHlsRequest
-    {
-        bool EnableAdaptiveBitrateStreaming { get; set; }
-    }
-
-    public class GetVariantHlsVideoPlaylist : VideoStreamRequest
-    {
-    }
-
-    public class GetVariantHlsAudioPlaylist : StreamRequest
-    {
-    }
-
-    public class GetHlsVideoSegment : VideoStreamRequest
-    {
-        public string PlaylistId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
-    public class GetHlsAudioSegment : StreamRequest
-    {
-        public string PlaylistId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
-    [Authenticated]
-    public class DynamicHlsService : BaseHlsService
-    {
-        public DynamicHlsService(
-            ILogger<DynamicHlsService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            INetworkManager networkManager,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-            NetworkManager = networkManager;
-        }
-
-        protected INetworkManager NetworkManager { get; private set; }
-
-        public Task<object> Get(GetMasterHlsVideoPlaylist request)
-        {
-            return GetMasterPlaylistInternal(request, "GET");
-        }
-
-        public Task<object> Head(GetMasterHlsVideoPlaylist request)
-        {
-            return GetMasterPlaylistInternal(request, "HEAD");
-        }
-
-        public Task<object> Get(GetMasterHlsAudioPlaylist request)
-        {
-            return GetMasterPlaylistInternal(request, "GET");
-        }
-
-        public Task<object> Head(GetMasterHlsAudioPlaylist request)
-        {
-            return GetMasterPlaylistInternal(request, "HEAD");
-        }
-
-        public Task<object> Get(GetVariantHlsVideoPlaylist request)
-        {
-            return GetVariantPlaylistInternal(request, true, "main");
-        }
-
-        public Task<object> Get(GetVariantHlsAudioPlaylist request)
-        {
-            return GetVariantPlaylistInternal(request, false, "main");
-        }
-
-        public Task<object> Get(GetHlsVideoSegment request)
-        {
-            return GetDynamicSegment(request, request.SegmentId);
-        }
-
-        public Task<object> Get(GetHlsAudioSegment request)
-        {
-            return GetDynamicSegment(request, request.SegmentId);
-        }
-
-        private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
-        {
-            if ((request.StartTimeTicks ?? 0) > 0)
-            {
-                throw new ArgumentException("StartTimeTicks is not allowed.");
-            }
-
-            var cancellationTokenSource = new CancellationTokenSource();
-            var cancellationToken = cancellationTokenSource.Token;
-
-            var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
-
-            var state = await GetState(request, cancellationToken).ConfigureAwait(false);
-
-            var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
-
-            var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
-
-            var segmentExtension = GetSegmentFileExtension(state.Request);
-
-            TranscodingJob job = null;
-
-            if (File.Exists(segmentPath))
-            {
-                job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-                Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
-                return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
-            }
-
-            var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
-            await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
-            var released = false;
-            var startTranscoding = false;
-
-            try
-            {
-                if (File.Exists(segmentPath))
-                {
-                    job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-                    transcodingLock.Release();
-                    released = true;
-                    Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
-                    return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
-                    var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
-                    if (currentTranscodingIndex == null)
-                    {
-                        Logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
-                        startTranscoding = true;
-                    }
-                    else if (requestedIndex < currentTranscodingIndex.Value)
-                    {
-                        Logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
-                        startTranscoding = true;
-                    }
-                    else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
-                    {
-                        Logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
-                        startTranscoding = true;
-                    }
-
-                    if (startTranscoding)
-                    {
-                        // If the playlist doesn't already exist, startup ffmpeg
-                        try
-                        {
-                            await ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
-
-                            if (currentTranscodingIndex.HasValue)
-                            {
-                                DeleteLastFile(playlistPath, segmentExtension, 0);
-                            }
-
-                            request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
-
-                            state.WaitForPath = segmentPath;
-                            job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
-                        }
-                        catch
-                        {
-                            state.Dispose();
-                            throw;
-                        }
-
-                        // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-                        if (job.TranscodingThrottler != null)
-                        {
-                            await job.TranscodingThrottler.UnpauseTranscoding();
-                        }
-                    }
-                }
-            }
-            finally
-            {
-                if (!released)
-                {
-                    transcodingLock.Release();
-                }
-            }
-
-            // Logger.LogInformation("waiting for {0}", segmentPath);
-            // while (!File.Exists(segmentPath))
-            //{
-            //    await Task.Delay(50, cancellationToken).ConfigureAwait(false);
-            //}
-
-            Logger.LogDebug("returning {0} [general case]", segmentPath);
-            job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-            return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
-        }
-
-        private const int BufferSize = 81920;
-
-        private long GetStartPositionTicks(StreamState state, int requestedIndex)
-        {
-            double startSeconds = 0;
-            var lengths = GetSegmentLengths(state);
-
-            if (requestedIndex >= lengths.Length)
-            {
-                var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
-                throw new ArgumentException(msg);
-            }
-
-            for (var i = 0; i < requestedIndex; i++)
-            {
-                startSeconds += lengths[i];
-            }
-
-            var position = TimeSpan.FromSeconds(startSeconds).Ticks;
-            return position;
-        }
-
-        private long GetEndPositionTicks(StreamState state, int requestedIndex)
-        {
-            double startSeconds = 0;
-            var lengths = GetSegmentLengths(state);
-
-            if (requestedIndex >= lengths.Length)
-            {
-                var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
-                throw new ArgumentException(msg);
-            }
-
-            for (var i = 0; i <= requestedIndex; i++)
-            {
-                startSeconds += lengths[i];
-            }
-
-            var position = TimeSpan.FromSeconds(startSeconds).Ticks;
-            return position;
-        }
-
-        private double[] GetSegmentLengths(StreamState state)
-        {
-            var result = new List<double>();
-
-            var ticks = state.RunTimeTicks ?? 0;
-
-            var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
-
-            while (ticks > 0)
-            {
-                var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
-
-                result.Add(TimeSpan.FromTicks(length).TotalSeconds);
-
-                ticks -= length;
-            }
-
-            return result.ToArray();
-        }
-
-        public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
-        {
-            var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
-
-            if (job == null || job.HasExited)
-            {
-                return null;
-            }
-
-            var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
-
-            if (file == null)
-            {
-                return null;
-            }
-
-            var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
-
-            var indexString = Path.GetFileNameWithoutExtension(file.Name).AsSpan().Slice(playlistFilename.Length);
-
-            return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
-        }
-
-        private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
-        {
-            var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
-
-            if (file != null)
-            {
-                DeleteFile(file.FullName, retryCount);
-            }
-        }
-
-        private void DeleteFile(string path, int retryCount)
-        {
-            if (retryCount >= 5)
-            {
-                return;
-            }
-
-            Logger.LogDebug("Deleting partial HLS file {path}", path);
-
-            try
-            {
-                FileSystem.DeleteFile(path);
-            }
-            catch (IOException ex)
-            {
-                Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
-
-                var task = Task.Delay(100);
-                Task.WaitAll(task);
-                DeleteFile(path, retryCount + 1);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
-            }
-        }
-
-        private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
-        {
-            var folder = Path.GetDirectoryName(playlist);
-
-            var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
-
-            try
-            {
-                return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
-                    .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
-                    .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
-                    .FirstOrDefault();
-            }
-            catch (IOException)
-            {
-                return null;
-            }
-        }
-
-        protected override int GetStartNumber(StreamState state)
-        {
-            return GetStartNumber(state.VideoRequest);
-        }
-
-        private int GetStartNumber(VideoStreamRequest request)
-        {
-            var segmentId = "0";
-
-            if (request is GetHlsVideoSegment segmentRequest)
-            {
-                segmentId = segmentRequest.SegmentId;
-            }
-
-            return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
-        }
-
-        private string GetSegmentPath(StreamState state, string playlist, int index)
-        {
-            var folder = Path.GetDirectoryName(playlist);
-
-            var filename = Path.GetFileNameWithoutExtension(playlist);
-
-            return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request));
-        }
-
-        private async Task<object> GetSegmentResult(StreamState state,
-            string playlistPath,
-            string segmentPath,
-            string segmentExtension,
-            int segmentIndex,
-            TranscodingJob transcodingJob,
-            CancellationToken cancellationToken)
-        {
-            var segmentExists = File.Exists(segmentPath);
-            if (segmentExists)
-            {
-                if (transcodingJob != null && transcodingJob.HasExited)
-                {
-                    // Transcoding job is over, so assume all existing files are ready
-                    Logger.LogDebug("serving up {0} as transcode is over", segmentPath);
-                    return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
-                }
-
-                var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
-
-                // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
-                if (segmentIndex < currentTranscodingIndex)
-                {
-                    Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
-                    return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
-                }
-            }
-
-            var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
-            if (transcodingJob != null)
-            {
-                while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
-                {
-                    // To be considered ready, the segment file has to exist AND
-                    // either the transcoding job should be done or next segment should also exist
-                    if (segmentExists)
-                    {
-                        if (transcodingJob.HasExited || File.Exists(nextSegmentPath))
-                        {
-                            Logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
-                            return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
-                        }
-                    }
-                    else
-                    {
-                        segmentExists = File.Exists(segmentPath);
-                        if (segmentExists)
-                        {
-                            continue; // avoid unnecessary waiting if segment just became available
-                        }
-                    }
-
-                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                }
-
-                if (!File.Exists(segmentPath))
-                {
-                    Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
-                }
-                else
-                {
-                    Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
-                }
-
-                cancellationToken.ThrowIfCancellationRequested();
-            }
-            else
-            {
-                Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
-            }
-
-            return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
-        }
-
-        private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
-        {
-            var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
-
-            return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            {
-                Path = segmentPath,
-                FileShare = FileShare.ReadWrite,
-                OnComplete = () =>
-                {
-                    Logger.LogDebug("finished serving {0}", segmentPath);
-                    if (transcodingJob != null)
-                    {
-                        transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
-                        ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
-                    }
-                }
-            });
-        }
-
-        private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
-        {
-            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
-            if (string.IsNullOrEmpty(request.MediaSourceId))
-            {
-                throw new ArgumentException("MediaSourceId is required");
-            }
-
-            var playlistText = string.Empty;
-
-            if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
-            {
-                var audioBitrate = state.OutputAudioBitrate ?? 0;
-                var videoBitrate = state.OutputVideoBitrate ?? 0;
-
-                playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
-            }
-
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-        }
-
-        private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
-        {
-            var builder = new StringBuilder();
-
-            builder.AppendLine("#EXTM3U");
-
-            var isLiveStream = state.IsSegmentedLiveStream;
-
-            var queryStringIndex = Request.RawUrl.IndexOf('?');
-            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
-            // from universal audio service
-            if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
-            {
-                queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
-            }
-            // from universal audio service
-            if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
-            {
-                queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
-            }
-
-            // Main stream
-            var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
-
-            playlistUrl += queryString;
-
-            var request = state.Request;
-
-            var subtitleStreams = state.MediaSource
-                .MediaStreams
-                .Where(i => i.IsTextSubtitleStream)
-                .ToList();
-
-            var subtitleGroup = subtitleStreams.Count > 0 &&
-                request is GetMasterHlsVideoPlaylist &&
-                (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ?
-                "subs" :
-                null;
-
-            // If we're burning in subtitles then don't add additional subs to the manifest
-            if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
-            {
-                subtitleGroup = null;
-            }
-
-            if (!string.IsNullOrWhiteSpace(subtitleGroup))
-            {
-                AddSubtitles(state, subtitleStreams, builder);
-            }
-
-            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
-            if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
-            {
-                var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
-                // By default, vary by just 200k
-                var variation = GetBitrateVariation(totalBitrate);
-
-                var newBitrate = totalBitrate - variation;
-                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
-                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-
-                variation *= 2;
-                newBitrate = totalBitrate - variation;
-                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
-                AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-            }
-
-            return builder.ToString();
-        }
-
-        private string ReplaceBitrate(string url, int oldValue, int newValue)
-        {
-            return url.Replace(
-                "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
-                "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
-                StringComparison.OrdinalIgnoreCase);
-        }
-
-        private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
-        {
-            var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
-
-            foreach (var stream in subtitles)
-            {
-                const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
-                var name = stream.DisplayTitle;
-
-                var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
-                var isForced = stream.IsForced;
-
-                var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
-                    state.Request.MediaSourceId,
-                    stream.Index.ToString(CultureInfo.InvariantCulture),
-                    30.ToString(CultureInfo.InvariantCulture),
-                    AuthorizationContext.GetAuthorizationInfo(Request).Token);
-
-                var line = string.Format(format,
-                    name,
-                    isDefault ? "YES" : "NO",
-                    isForced ? "YES" : "NO",
-                    url,
-                    stream.Language ?? "Unknown");
-
-                builder.AppendLine(line);
-            }
-        }
-
-        private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
-        {
-            // Within the local network this will likely do more harm than good.
-            if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
-            {
-                return false;
-            }
-
-            if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming)
-            {
-                return false;
-            }
-
-            if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
-            {
-                // Opening live streams is so slow it's not even worth it
-                return false;
-            }
-
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
-            {
-                return false;
-            }
-
-            if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
-            {
-                return false;
-            }
-
-            if (!state.IsOutputVideo)
-            {
-                return false;
-            }
-
-            // Having problems in android
-            return false;
-            // return state.VideoRequest.VideoBitRate.HasValue;
-        }
-
-        /// <summary>
-        /// Get the H.26X level of the output video stream.
-        /// </summary>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>H.26X level of the output video stream.</returns>
-        private int? GetOutputVideoCodecLevel(StreamState state)
-        {
-            string levelString;
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
-                && state.VideoStream.Level.HasValue)
-            {
-                levelString = state.VideoStream?.Level.ToString();
-            }
-            else
-            {
-                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
-            }
-
-            if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
-            {
-                return parsedLevel;
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>Formatted audio codec string.</returns>
-        private string GetPlaylistAudioCodecs(StreamState state)
-        {
-
-            if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
-            {
-                string profile = state.GetRequestedProfiles("aac").FirstOrDefault();
-
-                return HlsCodecStringFactory.GetAACString(profile);
-            }
-            else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringFactory.GetMP3String();
-            }
-            else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringFactory.GetAC3String();
-            }
-            else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
-            {
-                return HlsCodecStringFactory.GetEAC3String();
-            }
-
-            return string.Empty;
-        }
-
-        /// <summary>
-        /// Gets a formatted string of the output video codec, for use in the CODECS field.
-        /// </summary>
-        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="state">StreamState of the current stream.</param>
-        /// <returns>Formatted video codec string.</returns>
-        private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
-        {
-            if (level == 0)
-            {
-                // This is 0 when there's no requested H.26X level in the device profile
-                // and the source is not encoded in H.26X
-                Logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
-                return string.Empty;
-            }
-
-            if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
-            {
-                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
-
-                return HlsCodecStringFactory.GetH264String(profile, level);
-            }
-            else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
-            {
-                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
-                return HlsCodecStringFactory.GetH265String(profile, level);
-            }
-
-            return string.Empty;
-        }
-
-        /// <summary>
-        /// Appends a CODECS field containing formatted strings of
-        /// the active streams output video and audio codecs.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
-        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
-        {
-            // Video
-            string videoCodecs = string.Empty;
-            int? videoCodecLevel = GetOutputVideoCodecLevel(state);
-            if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
-            {
-                videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
-            }
-
-            // Audio
-            string audioCodecs = string.Empty;
-            if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
-            {
-                audioCodecs = GetPlaylistAudioCodecs(state);
-            }
-
-            StringBuilder codecs = new StringBuilder();
-
-            codecs.Append(videoCodecs)
-                .Append(',')
-                .Append(audioCodecs);
-
-            if (codecs.Length > 1)
-            {
-                builder.Append(",CODECS=\"")
-                    .Append(codecs)
-                    .Append('"');
-            }
-        }
-
-        /// <summary>
-        /// Appends a FRAME-RATE field containing the framerate of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
-        {
-            double? framerate = null;
-            if (state.TargetFramerate.HasValue)
-            {
-                framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
-            }
-            else if (state.VideoStream?.RealFrameRate != null)
-            {
-                framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
-            }
-
-            if (framerate.HasValue)
-            {
-                builder.Append(",FRAME-RATE=")
-                    .Append(framerate.Value);
-            }
-        }
-
-        /// <summary>
-        /// Appends a RESOLUTION field containing the resolution of the output stream.
-        /// </summary>
-        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
-        /// <param name="builder">StringBuilder to append the field to.</param>
-        /// <param name="state">StreamState of the current stream.</param>
-        private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
-        {
-            if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
-            {
-                builder.Append(",RESOLUTION=")
-                    .Append(state.OutputWidth.GetValueOrDefault())
-                    .Append('x')
-                    .Append(state.OutputHeight.GetValueOrDefault());
-            }
-        }
-
-        private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
-        {
-            builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
-                .Append(bitrate.ToString(CultureInfo.InvariantCulture))
-                .Append(",AVERAGE-BANDWIDTH=")
-                .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
-            AppendPlaylistCodecsField(builder, state);
-
-            AppendPlaylistResolutionField(builder, state);
-
-            AppendPlaylistFramerateField(builder, state);
-
-            if (!string.IsNullOrWhiteSpace(subtitleGroup))
-            {
-                builder.Append(",SUBTITLES=\"")
-                    .Append(subtitleGroup)
-                    .Append('"');
-            }
-
-            builder.Append(Environment.NewLine);
-            builder.AppendLine(url);
-        }
-
-        private int GetBitrateVariation(int bitrate)
-        {
-            // By default, vary by just 50k
-            var variation = 50000;
-
-            if (bitrate >= 10000000)
-            {
-                variation = 2000000;
-            }
-            else if (bitrate >= 5000000)
-            {
-                variation = 1500000;
-            }
-            else if (bitrate >= 3000000)
-            {
-                variation = 1000000;
-            }
-            else if (bitrate >= 2000000)
-            {
-                variation = 500000;
-            }
-            else if (bitrate >= 1000000)
-            {
-                variation = 300000;
-            }
-            else if (bitrate >= 600000)
-            {
-                variation = 200000;
-            }
-            else if (bitrate >= 400000)
-            {
-                variation = 100000;
-            }
-
-            return variation;
-        }
-
-        private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
-        {
-            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
-            var segmentLengths = GetSegmentLengths(state);
-
-            var builder = new StringBuilder();
-
-            builder.AppendLine("#EXTM3U");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
-            builder.AppendLine("#EXT-X-VERSION:3");
-            builder.Append("#EXT-X-TARGETDURATION:")
-                .AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
-            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-
-            var queryStringIndex = Request.RawUrl.IndexOf('?');
-            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
-            // if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
-            //{
-            //    queryString = string.Empty;
-            //}
-
-            var index = 0;
-
-            foreach (var length in segmentLengths)
-            {
-                builder.Append("#EXTINF:")
-                    .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
-                    .AppendLine(", nodesc");
-
-                builder.AppendFormat(
-                    CultureInfo.InvariantCulture,
-                    "hls1/{0}/{1}{2}{3}",
-                    name,
-                    index.ToString(CultureInfo.InvariantCulture),
-                    GetSegmentFileExtension(request),
-                    queryString).AppendLine();
-
-                index++;
-            }
-
-            builder.AppendLine("#EXT-X-ENDLIST");
-
-            var playlistText = builder.ToString();
-
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
-        }
-
-        protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
-        {
-            var audioCodec = EncodingHelper.GetAudioEncoder(state);
-
-            if (!state.IsOutputVideo)
-            {
-                if (EncodingHelper.IsCopyCodec(audioCodec))
-                {
-                    return "-acodec copy";
-                }
-
-                var audioTranscodeParams = new List<string>();
-
-                audioTranscodeParams.Add("-acodec " + audioCodec);
-
-                if (state.OutputAudioBitrate.HasValue)
-                {
-                    audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
-                }
-
-                if (state.OutputAudioChannels.HasValue)
-                {
-                    audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
-                }
-
-                if (state.OutputAudioSampleRate.HasValue)
-                {
-                    audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
-                }
-
-                audioTranscodeParams.Add("-vn");
-                return string.Join(" ", audioTranscodeParams.ToArray());
-            }
-
-            if (EncodingHelper.IsCopyCodec(audioCodec))
-            {
-                var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-                if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
-                {
-                    return "-codec:a:0 copy -copypriorss:a:0 0";
-                }
-
-                return "-codec:a:0 copy";
-            }
-
-            var args = "-codec:a:0 " + audioCodec;
-
-            var channels = state.OutputAudioChannels;
-
-            if (channels.HasValue)
-            {
-                args += " -ac " + channels.Value;
-            }
-
-            var bitrate = state.OutputAudioBitrate;
-
-            if (bitrate.HasValue)
-            {
-                args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            if (state.OutputAudioSampleRate.HasValue)
-            {
-                args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
-            return args;
-        }
-
-        protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
-        {
-            if (!state.IsOutputVideo)
-            {
-                return string.Empty;
-            }
-
-            var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-            var args = "-codec:v:0 " + codec;
-
-            // if (state.EnableMpegtsM2TsMode)
-            // {
-            //     args += " -mpegts_m2ts_mode 1";
-            // }
-
-            // See if we can save come cpu cycles by avoiding encoding
-            if (EncodingHelper.IsCopyCodec(codec))
-            {
-                if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
-                {
-                    string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
-                    if (!string.IsNullOrEmpty(bitStreamArgs))
-                    {
-                        args += " " + bitStreamArgs;
-                    }
-                }
-
-                // args += " -flags -global_header";
-            }
-            else
-            {
-                var gopArg = string.Empty;
-                var keyFrameArg = string.Format(
-                    CultureInfo.InvariantCulture,
-                    " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
-                    GetStartNumber(state) * state.SegmentLength,
-                    state.SegmentLength);
-
-                var framerate = state.VideoStream?.RealFrameRate;
-
-                if (framerate.HasValue)
-                {
-                    // This is to make sure keyframe interval is limited to our segment,
-                    // as forcing keyframes is not enough.
-                    // Example: we encoded half of desired length, then codec detected
-                    // scene cut and inserted a keyframe; next forced keyframe would
-                    // be created outside of segment, which breaks seeking
-                    // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
-                    gopArg = string.Format(
-                        CultureInfo.InvariantCulture,
-                        " -g {0} -keyint_min {0} -sc_threshold 0",
-                        Math.Ceiling(state.SegmentLength * framerate.Value)
-                    );
-                }
-
-                args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset());
-
-                // Unable to force key frames using these hw encoders, set key frames by GOP
-                if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
-                {
-                    args += " " + gopArg;
-                }
-                else
-                {
-                    args += " " + keyFrameArg + gopArg;
-                }
-
-                // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
-                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
-                // This is for graphical subs
-                if (hasGraphicalSubs)
-                {
-                    args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
-                }
-                // Add resolution params, if specified
-                else
-                {
-                    args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
-                }
-
-                // -start_at_zero is necessary to use with -ss when seeking,
-                // otherwise the target position cannot be determined.
-                if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
-                {
-                    args += " -start_at_zero";
-                }
-
-                // args += " -flags -global_header";
-            }
-
-            if (!string.IsNullOrEmpty(state.OutputVideoSync))
-            {
-                args += " -vsync " + state.OutputVideoSync;
-            }
-
-            args += EncodingHelper.GetOutputFFlags(state);
-
-            return args;
-        }
-
-        protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
-        {
-            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
-
-            if (state.BaseRequest.BreakOnNonKeyFrames)
-            {
-                // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
-                //        breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
-                //        to produce a missing part of video stream before first keyframe is encountered, which may lead to
-                //        awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
-                Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
-                state.BaseRequest.BreakOnNonKeyFrames = false;
-            }
-
-            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
-            // If isEncoding is true we're actually starting ffmpeg
-            var startNumber = GetStartNumber(state);
-            var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
-
-            var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
-
-            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
-
-            var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
-            if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
-            {
-                segmentFormat = "mpegts";
-            }
-
-            return string.Format(
-                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
-                inputModifier,
-                EncodingHelper.GetInputArgument(state, encodingOptions),
-                threads,
-                mapArgs,
-                GetVideoArguments(state, encodingOptions),
-                GetAudioArguments(state, encodingOptions),
-                state.SegmentLength.ToString(CultureInfo.InvariantCulture),
-                segmentFormat,
-                startNumberParam,
-                outputTsArg,
-                outputPath
-            ).Trim();
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
deleted file mode 100644
index 3bbb77a65e..0000000000
--- a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-using System;
-using System.Text;
-
-
-namespace MediaBrowser.Api.Playback
-{
-    /// <summary>
-    /// Get various codec strings for use in HLS playlists.
-    /// </summary>
-    static class HlsCodecStringFactory
-    {
-
-        /// <summary>
-        /// Gets a MP3 codec string.
-        /// </summary>
-        /// <returns>MP3 codec string.</returns>
-        public static string GetMP3String()
-        {
-            return "mp4a.40.34";
-        }
-
-        /// <summary>
-        /// Gets an AAC codec string.
-        /// </summary>
-        /// <param name="profile">AAC profile.</param>
-        /// <returns>AAC codec string.</returns>
-        public static string GetAACString(string profile)
-        {
-            StringBuilder result = new StringBuilder("mp4a", 9);
-
-            if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
-            {
-                result.Append(".40.5");
-            }
-            else
-            {
-                // Default to LC if profile is invalid
-                result.Append(".40.2");
-            }
-
-            return result.ToString();
-        }
-
-        /// <summary>
-        /// Gets a H.264 codec string.
-        /// </summary>
-        /// <param name="profile">H.264 profile.</param>
-        /// <param name="level">H.264 level.</param>
-        /// <returns>H.264 string.</returns>
-        public static string GetH264String(string profile, int level)
-        {
-            StringBuilder result = new StringBuilder("avc1", 11);
-
-            if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
-            {
-                result.Append(".6400");
-            }
-            else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
-            {
-                result.Append(".4D40");
-            }
-            else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
-            {
-                result.Append(".42E0");
-            }
-            else
-            {
-                // Default to constrained baseline if profile is invalid
-                result.Append(".4240");
-            }
-
-            string levelHex = level.ToString("X2");
-            result.Append(levelHex);
-
-            return result.ToString();
-        }
-
-        /// <summary>
-        /// Gets a H.265 codec string.
-        /// </summary>
-        /// <param name="profile">H.265 profile.</param>
-        /// <param name="level">H.265 level.</param>
-        /// <returns>H.265 string.</returns>
-        public static string GetH265String(string profile, int level)
-        {
-            // The h265 syntax is a bit of a mystery at the time this comment was written.
-            // This is what I've found through various sources:
-            // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
-            StringBuilder result = new StringBuilder("hev1", 16);
-
-            if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
-            {
-                result.Append(".2.6");
-            }
-            else
-            {
-                // Default to main if profile is invalid
-                result.Append(".1.6");
-            }
-
-            result.Append(".L")
-                .Append(level * 3)
-                .Append(".B0");
-
-            return result.ToString();
-        }
-
-        /// <summary>
-        /// Gets an AC-3 codec string.
-        /// </summary>
-        /// <returns>AC-3 codec string.</returns>
-        public static string GetAC3String()
-        {
-            return "mp4a.a5";
-        }
-
-        /// <summary>
-        /// Gets an E-AC-3 codec string.
-        /// </summary>
-        /// <returns>E-AC-3 codec string.</returns>
-        public static string GetEAC3String()
-        {
-            return "mp4a.a6";
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
deleted file mode 100644
index 4487522c1b..0000000000
--- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace MediaBrowser.Api.Playback.Hls
-{
-    public class GetLiveHlsStream : VideoStreamRequest
-    {
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
deleted file mode 100644
index 2ebf0e420d..0000000000
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ /dev/null
@@ -1,442 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
-    /// <summary>
-    /// Class BaseProgressiveStreamingService.
-    /// </summary>
-    public abstract class BaseProgressiveStreamingService : BaseStreamingService
-    {
-        protected IHttpClient HttpClient { get; private set; }
-
-        public BaseProgressiveStreamingService(
-            ILogger<BaseProgressiveStreamingService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IHttpClient httpClient,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-            HttpClient = httpClient;
-        }
-
-        /// <summary>
-        /// Gets the output file extension.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected override string GetOutputFileExtension(StreamState state)
-        {
-            var ext = base.GetOutputFileExtension(state);
-
-            if (!string.IsNullOrEmpty(ext))
-            {
-                return ext;
-            }
-
-            var isVideoRequest = state.VideoRequest != null;
-
-            // Try to infer based on the desired video codec
-            if (isVideoRequest)
-            {
-                var videoCodec = state.VideoRequest.VideoCodec;
-
-                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".ts";
-                }
-
-                if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".ogv";
-                }
-
-                if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".webm";
-                }
-
-                if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".asf";
-                }
-            }
-
-            // Try to infer based on the desired audio codec
-            if (!isVideoRequest)
-            {
-                var audioCodec = state.Request.AudioCodec;
-
-                if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".aac";
-                }
-
-                if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".mp3";
-                }
-
-                if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".ogg";
-                }
-
-                if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
-                {
-                    return ".wma";
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets the type of the transcoding job.
-        /// </summary>
-        /// <value>The type of the transcoding job.</value>
-        protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
-
-        /// <summary>
-        /// Processes the request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <returns>Task.</returns>
-        protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
-        {
-            var cancellationTokenSource = new CancellationTokenSource();
-
-            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
-
-            var responseHeaders = new Dictionary<string, string>();
-
-            if (request.Static && state.DirectStreamProvider != null)
-            {
-                AddDlnaHeaders(state, responseHeaders, true);
-
-                using (state)
-                {
-                    var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-                    // TODO: Don't hardcode this
-                    outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
-
-                    return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
-                    {
-                        AllowEndOfFile = false
-                    };
-                }
-            }
-
-            // Static remote stream
-            if (request.Static && state.InputProtocol == MediaProtocol.Http)
-            {
-                AddDlnaHeaders(state, responseHeaders, true);
-
-                using (state)
-                {
-                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
-                }
-            }
-
-            if (request.Static && state.InputProtocol != MediaProtocol.File)
-            {
-                throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
-            }
-
-            var outputPath = state.OutputFilePath;
-            var outputPathExists = File.Exists(outputPath);
-
-            var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
-            var isTranscodeCached = outputPathExists && transcodingJob != null;
-
-            AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
-
-            // Static stream
-            if (request.Static)
-            {
-                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
-
-                using (state)
-                {
-                    if (state.MediaSource.IsInfiniteStream)
-                    {
-                        var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-                        {
-                            [HeaderNames.ContentType] = contentType
-                        };
-
-
-                        return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, CancellationToken.None)
-                        {
-                            AllowEndOfFile = false
-                        };
-                    }
-
-                    TimeSpan? cacheDuration = null;
-
-                    if (!string.IsNullOrEmpty(request.Tag))
-                    {
-                        cacheDuration = TimeSpan.FromDays(365);
-                    }
-
-                    return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-                    {
-                        ResponseHeaders = responseHeaders,
-                        ContentType = contentType,
-                        IsHeadRequest = isHeadRequest,
-                        Path = state.MediaPath,
-                        CacheDuration = cacheDuration
-
-                    }).ConfigureAwait(false);
-                }
-            }
-
-            //// Not static but transcode cache file exists
-            // if (isTranscodeCached && state.VideoRequest == null)
-            //{
-            //    var contentType = state.GetMimeType(outputPath);
-
-            //    try
-            //    {
-            //        if (transcodingJob != null)
-            //        {
-            //            ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
-            //        }
-
-            //        return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-            //        {
-            //            ResponseHeaders = responseHeaders,
-            //            ContentType = contentType,
-            //            IsHeadRequest = isHeadRequest,
-            //            Path = outputPath,
-            //            FileShare = FileShare.ReadWrite,
-            //            OnComplete = () =>
-            //            {
-            //                if (transcodingJob != null)
-            //                {
-            //                    ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
-            //                }
-            //            }
-
-            //        }).ConfigureAwait(false);
-            //    }
-            //    finally
-            //    {
-            //        state.Dispose();
-            //    }
-            //}
-
-            // Need to start ffmpeg
-            try
-            {
-                return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
-            }
-            catch
-            {
-                state.Dispose();
-
-                throw;
-            }
-        }
-
-        /// <summary>
-        /// Gets the static remote stream result.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <param name="cancellationTokenSource">The cancellation token source.</param>
-        /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetStaticRemoteStreamResult(
-            StreamState state,
-            Dictionary<string, string> responseHeaders,
-            bool isHeadRequest,
-            CancellationTokenSource cancellationTokenSource)
-        {
-            var options = new HttpRequestOptions
-            {
-                Url = state.MediaPath,
-                BufferContent = false,
-                CancellationToken = cancellationTokenSource.Token
-            };
-
-            if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
-            {
-                options.UserAgent = useragent;
-            }
-
-            var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
-
-            responseHeaders[HeaderNames.AcceptRanges] = "none";
-
-            // Seeing cases of -1 here
-            if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
-            {
-                responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            if (isHeadRequest)
-            {
-                using (response)
-                {
-                    return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
-                }
-            }
-
-            var result = new StaticRemoteStreamWriter(response);
-
-            result.Headers[HeaderNames.ContentType] = response.ContentType;
-
-            // Add the response headers to the result object
-            foreach (var header in responseHeaders)
-            {
-                result.Headers[header.Key] = header.Value;
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Gets the stream result.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="responseHeaders">The response headers.</param>
-        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <param name="cancellationTokenSource">The cancellation token source.</param>
-        /// <returns>Task{System.Object}.</returns>
-        private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
-        {
-            // Use the command line args with a dummy playlist path
-            var outputPath = state.OutputFilePath;
-
-            responseHeaders[HeaderNames.AcceptRanges] = "none";
-
-            var contentType = state.GetMimeType(outputPath);
-
-            // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
-            var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
-
-            if (contentLength.HasValue)
-            {
-                responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            // Headers only
-            if (isHeadRequest)
-            {
-                var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
-
-                if (streamResult is IHasHeaders hasHeaders)
-                {
-                    if (contentLength.HasValue)
-                    {
-                        hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
-                    }
-                    else
-                    {
-                        hasHeaders.Headers.Remove(HeaderNames.ContentLength);
-                    }
-                }
-
-                return streamResult;
-            }
-
-            var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
-            await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
-            try
-            {
-                TranscodingJob job;
-
-                if (!File.Exists(outputPath))
-                {
-                    job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
-                }
-                else
-                {
-                    job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
-                    state.Dispose();
-                }
-
-                var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
-                {
-                    [HeaderNames.ContentType] = contentType
-                };
-
-
-                // Add the response headers to the result object
-                foreach (var item in responseHeaders)
-                {
-                    outputHeaders[item.Key] = item.Value;
-                }
-
-                return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
-            }
-            finally
-            {
-                transcodingLock.Release();
-            }
-        }
-
-        /// <summary>
-        /// Gets the length of the estimated content.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.Nullable{System.Int64}.</returns>
-        private long? GetEstimatedContentLength(StreamState state)
-        {
-            var totalBitrate = state.TotalOutputBitrate ?? 0;
-
-            if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
-            {
-                return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
-            }
-
-            return null;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
deleted file mode 100644
index b70fff128b..0000000000
--- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
+++ /dev/null
@@ -1,182 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
-    public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
-    {
-        private readonly IFileSystem _fileSystem;
-        private readonly TranscodingJob _job;
-        private readonly ILogger _logger;
-        private readonly string _path;
-        private readonly CancellationToken _cancellationToken;
-        private readonly Dictionary<string, string> _outputHeaders;
-
-        private long _bytesWritten = 0;
-        public long StartPosition { get; set; }
-
-        public bool AllowEndOfFile = true;
-
-        private readonly IDirectStreamProvider _directStreamProvider;
-
-        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
-        {
-            _fileSystem = fileSystem;
-            _path = path;
-            _outputHeaders = outputHeaders;
-            _job = job;
-            _logger = logger;
-            _cancellationToken = cancellationToken;
-        }
-
-        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
-        {
-            _directStreamProvider = directStreamProvider;
-            _outputHeaders = outputHeaders;
-            _job = job;
-            _logger = logger;
-            _cancellationToken = cancellationToken;
-        }
-
-        public IDictionary<string, string> Headers => _outputHeaders;
-
-        private Stream GetInputStream(bool allowAsyncFileRead)
-        {
-            var fileOptions = FileOptions.SequentialScan;
-
-            if (allowAsyncFileRead)
-            {
-                fileOptions |= FileOptions.Asynchronous;
-            }
-
-            return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
-        }
-
-        public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
-        {
-            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
-
-            try
-            {
-                if (_directStreamProvider != null)
-                {
-                    await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-
-                var eofCount = 0;
-
-                // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-                var allowAsyncFileRead = OperatingSystem.Id != OperatingSystemId.Windows;
-
-                using (var inputStream = GetInputStream(allowAsyncFileRead))
-                {
-                    if (StartPosition > 0)
-                    {
-                        inputStream.Position = StartPosition;
-                    }
-
-                    while (eofCount < 20 || !AllowEndOfFile)
-                    {
-                        int bytesRead;
-                        if (allowAsyncFileRead)
-                        {
-                            bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-                        }
-                        else
-                        {
-                            bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
-                        }
-
-                        // var position = fs.Position;
-                        // _logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
-
-                        if (bytesRead == 0)
-                        {
-                            if (_job == null || _job.HasExited)
-                            {
-                                eofCount++;
-                            }
-
-                            await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                        }
-                        else
-                        {
-                            eofCount = 0;
-                        }
-                    }
-                }
-            }
-            finally
-            {
-                if (_job != null)
-                {
-                    ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
-                }
-            }
-        }
-
-        private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            var array = new byte[IODefaults.CopyToBufferSize];
-            int bytesRead;
-            int totalBytesRead = 0;
-
-            while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
-            {
-                var bytesToWrite = bytesRead;
-
-                if (bytesToWrite > 0)
-                {
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                    _bytesWritten += bytesRead;
-                    totalBytesRead += bytesRead;
-
-                    if (_job != null)
-                    {
-                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
-                    }
-                }
-            }
-
-            return totalBytesRead;
-        }
-
-        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            var array = new byte[IODefaults.CopyToBufferSize];
-            int bytesRead;
-            int totalBytesRead = 0;
-
-            while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
-            {
-                var bytesToWrite = bytesRead;
-
-                if (bytesToWrite > 0)
-                {
-                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                    _bytesWritten += bytesRead;
-                    totalBytesRead += bytesRead;
-
-                    if (_job != null)
-                    {
-                        _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
-                    }
-                }
-            }
-
-            return totalBytesRead;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
deleted file mode 100644
index 5bc85f42d2..0000000000
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Progressive
-{
-    public class GetVideoStream : VideoStreamRequest
-    {
-    }
-
-    /// <summary>
-    /// Class VideoService.
-    /// </summary>
-    // TODO: In order to autheneticate this in the future, Dlna playback will require updating
-    //[Authenticated]
-    public class VideoService : BaseProgressiveStreamingService
-    {
-        public VideoService(
-            ILogger<VideoService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory,
-            IHttpClient httpClient,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IIsoManager isoManager,
-            IMediaEncoder mediaEncoder,
-            IFileSystem fileSystem,
-            IDlnaManager dlnaManager,
-            IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager,
-            IJsonSerializer jsonSerializer,
-            IAuthorizationContext authorizationContext,
-            EncodingHelper encodingHelper)
-            : base(
-                logger,
-                serverConfigurationManager,
-                httpResultFactory,
-                httpClient,
-                userManager,
-                libraryManager,
-                isoManager,
-                mediaEncoder,
-                fileSystem,
-                dlnaManager,
-                deviceManager,
-                mediaSourceManager,
-                jsonSerializer,
-                authorizationContext,
-                encodingHelper)
-        {
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Get(GetVideoStream request)
-        {
-            return ProcessRequest(request, false);
-        }
-
-        /// <summary>
-        /// Heads the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public Task<object> Head(GetVideoStream request)
-        {
-            return ProcessRequest(request, true);
-        }
-
-        protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
-        {
-            return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset());
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
deleted file mode 100644
index 7e2e337ad1..0000000000
--- a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Playback
-{
-    /// <summary>
-    /// Class StaticRemoteStreamWriter.
-    /// </summary>
-    public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders
-    {
-        /// <summary>
-        /// The _input stream.
-        /// </summary>
-        private readonly HttpResponseInfo _response;
-
-        /// <summary>
-        /// The _options.
-        /// </summary>
-        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
-        public StaticRemoteStreamWriter(HttpResponseInfo response)
-        {
-            _response = response;
-        }
-
-        /// <summary>
-        /// Gets the options.
-        /// </summary>
-        /// <value>The options.</value>
-        public IDictionary<string, string> Headers => _options;
-
-        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
-        {
-            using (_response)
-            {
-                await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs
deleted file mode 100644
index 67c334e489..0000000000
--- a/MediaBrowser.Api/Playback/StreamRequest.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Api.Playback
-{
-    /// <summary>
-    /// Class StreamRequest.
-    /// </summary>
-    public class StreamRequest : BaseEncodingJobOptions
-    {
-        [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public string DeviceProfileId { get; set; }
-
-        public string Params { get; set; }
-
-        public string PlaySessionId { get; set; }
-
-        public string Tag { get; set; }
-
-        public string SegmentContainer { get; set; }
-
-        public int? SegmentLength { get; set; }
-
-        public int? MinSegments { get; set; }
-    }
-
-    public class VideoStreamRequest : StreamRequest
-    {
-        /// <summary>
-        /// Gets a value indicating whether this instance has fixed resolution.
-        /// </summary>
-        /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
-        public bool HasFixedResolution => Width.HasValue || Height.HasValue;
-
-        public bool EnableSubtitlesInManifest { get; set; }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
deleted file mode 100644
index c244b00334..0000000000
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dlna;
-
-namespace MediaBrowser.Api.Playback
-{
-    public class StreamState : EncodingJobInfo, IDisposable
-    {
-        private readonly IMediaSourceManager _mediaSourceManager;
-        private bool _disposed = false;
-
-        public string RequestedUrl { get; set; }
-
-        public StreamRequest Request
-        {
-            get => (StreamRequest)BaseRequest;
-            set
-            {
-                BaseRequest = value;
-
-                IsVideoRequest = VideoRequest != null;
-            }
-        }
-
-        public TranscodingThrottler TranscodingThrottler { get; set; }
-
-        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
-
-        public IDirectStreamProvider DirectStreamProvider { get; set; }
-
-        public string WaitForPath { get; set; }
-
-        public bool IsOutputVideo => Request is VideoStreamRequest;
-
-        public int SegmentLength
-        {
-            get
-            {
-                if (Request.SegmentLength.HasValue)
-                {
-                    return Request.SegmentLength.Value;
-                }
-
-                if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
-                {
-                    var userAgent = UserAgent ?? string.Empty;
-
-                    if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
-                        userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        if (IsSegmentedLiveStream)
-                        {
-                            return 6;
-                        }
-
-                        return 6;
-                    }
-
-                    if (IsSegmentedLiveStream)
-                    {
-                        return 3;
-                    }
-
-                    return 6;
-                }
-
-                return 3;
-            }
-        }
-
-        public int MinSegments
-        {
-            get
-            {
-                if (Request.MinSegments.HasValue)
-                {
-                    return Request.MinSegments.Value;
-                }
-
-                return SegmentLength >= 10 ? 2 : 3;
-            }
-        }
-
-        public string UserAgent { get; set; }
-
-        public bool EstimateContentLength { get; set; }
-
-        public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
-        public bool EnableDlnaHeaders { get; set; }
-
-        public DeviceProfile DeviceProfile { get; set; }
-
-        public TranscodingJob TranscodingJob { get; set; }
-
-        public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
-            : base(transcodingType)
-        {
-            _mediaSourceManager = mediaSourceManager;
-        }
-
-        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
-        {
-            ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
-        }
-
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                // REVIEW: Is this the right place for this?
-                if (MediaSource.RequiresClosing
-                    && string.IsNullOrWhiteSpace(Request.LiveStreamId)
-                    && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
-                {
-                    _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
-                }
-
-                TranscodingThrottler?.Dispose();
-            }
-
-            TranscodingThrottler = null;
-            TranscodingJob = null;
-
-            _disposed = true;
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/MediaBrowser.Api/Playback/TranscodingThrottler.cs
deleted file mode 100644
index 0e73d77efd..0000000000
--- a/MediaBrowser.Api/Playback/TranscodingThrottler.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
-    public class TranscodingThrottler : IDisposable
-    {
-        private readonly TranscodingJob _job;
-        private readonly ILogger _logger;
-        private Timer _timer;
-        private bool _isPaused;
-        private readonly IConfigurationManager _config;
-        private readonly IFileSystem _fileSystem;
-
-        public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem)
-        {
-            _job = job;
-            _logger = logger;
-            _config = config;
-            _fileSystem = fileSystem;
-        }
-
-        private EncodingOptions GetOptions()
-        {
-            return _config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
-        public void Start()
-        {
-            _timer = new Timer(TimerCallback, null, 5000, 5000);
-        }
-
-        private async void TimerCallback(object state)
-        {
-            if (_job.HasExited)
-            {
-                DisposeTimer();
-                return;
-            }
-
-            var options = GetOptions();
-
-            if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
-            {
-                await PauseTranscoding();
-            }
-            else
-            {
-                await UnpauseTranscoding();
-            }
-        }
-
-        private async Task PauseTranscoding()
-        {
-            if (!_isPaused)
-            {
-                _logger.LogDebug("Sending pause command to ffmpeg");
-
-                try
-                {
-                    await _job.Process.StandardInput.WriteAsync("c");
-                    _isPaused = true;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error pausing transcoding");
-                }
-            }
-        }
-
-        public async Task UnpauseTranscoding()
-        {
-            if (_isPaused)
-            {
-                _logger.LogDebug("Sending resume command to ffmpeg");
-
-                try
-                {
-                    await _job.Process.StandardInput.WriteLineAsync();
-                    _isPaused = false;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error resuming transcoding");
-                }
-            }
-        }
-
-        private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
-        {
-            var bytesDownloaded = job.BytesDownloaded ?? 0;
-            var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
-            var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
-
-            var path = job.Path;
-            var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
-
-            if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
-            {
-                // HLS - time-based consideration
-
-                var targetGap = gapLengthInTicks;
-                var gap = transcodingPositionTicks - downloadPositionTicks;
-
-                if (gap < targetGap)
-                {
-                    _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
-                    return false;
-                }
-
-                _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
-                return true;
-            }
-
-            if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
-            {
-                // Progressive Streaming - byte-based consideration
-
-                try
-                {
-                    var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
-
-                    // Estimate the bytes the transcoder should be ahead
-                    double gapFactor = gapLengthInTicks;
-                    gapFactor /= transcodingPositionTicks;
-                    var targetGap = bytesTranscoded * gapFactor;
-
-                    var gap = bytesTranscoded - bytesDownloaded;
-
-                    if (gap < targetGap)
-                    {
-                        _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
-                        return false;
-                    }
-
-                    _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
-                    return true;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error getting output size");
-                    return false;
-                }
-            }
-
-            _logger.LogDebug("No throttle data for " + path);
-            return false;
-        }
-
-        public async Task Stop()
-        {
-            DisposeTimer();
-            await UnpauseTranscoding();
-        }
-
-        public void Dispose()
-        {
-            DisposeTimer();
-        }
-
-        private void DisposeTimer()
-        {
-            if (_timer != null)
-            {
-                _timer.Dispose();
-                _timer = null;
-            }
-        }
-    }
-}
diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs
deleted file mode 100644
index 078af3e305..0000000000
--- a/MediaBrowser.Api/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.Api")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright ©  2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components.  If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
diff --git a/MediaBrowser.Api/TestService.cs b/MediaBrowser.Api/TestService.cs
deleted file mode 100644
index 6c999e08d1..0000000000
--- a/MediaBrowser.Api/TestService.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Service for testing path value.
-    /// </summary>
-    public class TestService : BaseApiService
-    {
-        /// <summary>
-        /// Test service.
-        /// </summary>
-        /// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param>
-        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param>
-        public TestService(
-            ILogger<TestService> logger,
-            IServerConfigurationManager serverConfigurationManager,
-            IHttpResultFactory httpResultFactory)
-            : base(logger, serverConfigurationManager, httpResultFactory)
-        {
-        }
-    }
-}
diff --git a/MediaBrowser.Api/TranscodingJob.cs b/MediaBrowser.Api/TranscodingJob.cs
deleted file mode 100644
index bfc311a272..0000000000
--- a/MediaBrowser.Api/TranscodingJob.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-using MediaBrowser.Api.Playback;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Dto;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Class TranscodingJob.
-    /// </summary>
-    public class TranscodingJob
-    {
-        /// <summary>
-        /// Gets or sets the play session identifier.
-        /// </summary>
-        /// <value>The play session identifier.</value>
-        public string PlaySessionId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the live stream identifier.
-        /// </summary>
-        /// <value>The live stream identifier.</value>
-        public string LiveStreamId { get; set; }
-
-        public bool IsLiveOutput { get; set; }
-
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public MediaSourceInfo MediaSource { get; set; }
-
-        public string Path { get; set; }
-        /// <summary>
-        /// Gets or sets the type.
-        /// </summary>
-        /// <value>The type.</value>
-        public TranscodingJobType Type { get; set; }
-        /// <summary>
-        /// Gets or sets the process.
-        /// </summary>
-        /// <value>The process.</value>
-        public Process Process { get; set; }
-
-        public ILogger Logger { get; private set; }
-        /// <summary>
-        /// Gets or sets the active request count.
-        /// </summary>
-        /// <value>The active request count.</value>
-        public int ActiveRequestCount { get; set; }
-        /// <summary>
-        /// Gets or sets the kill timer.
-        /// </summary>
-        /// <value>The kill timer.</value>
-        private Timer KillTimer { get; set; }
-
-        public string DeviceId { get; set; }
-
-        public CancellationTokenSource CancellationTokenSource { get; set; }
-
-        public object ProcessLock = new object();
-
-        public bool HasExited { get; set; }
-
-        public bool IsUserPaused { get; set; }
-
-        public string Id { get; set; }
-
-        public float? Framerate { get; set; }
-
-        public double? CompletionPercentage { get; set; }
-
-        public long? BytesDownloaded { get; set; }
-
-        public long? BytesTranscoded { get; set; }
-
-        public int? BitRate { get; set; }
-
-        public long? TranscodingPositionTicks { get; set; }
-
-        public long? DownloadPositionTicks { get; set; }
-
-        public TranscodingThrottler TranscodingThrottler { get; set; }
-
-        private readonly object _timerLock = new object();
-
-        public DateTime LastPingDate { get; set; }
-
-        public int PingTimeout { get; set; }
-
-        public TranscodingJob(ILogger logger)
-        {
-            Logger = logger;
-        }
-
-        public void StopKillTimer()
-        {
-            lock (_timerLock)
-            {
-                KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
-            }
-        }
-
-        public void DisposeKillTimer()
-        {
-            lock (_timerLock)
-            {
-                if (KillTimer != null)
-                {
-                    KillTimer.Dispose();
-                    KillTimer = null;
-                }
-            }
-        }
-
-        public void StartKillTimer(Action<object> callback)
-        {
-            StartKillTimer(callback, PingTimeout);
-        }
-
-        public void StartKillTimer(Action<object> callback, int intervalMs)
-        {
-            if (HasExited)
-            {
-                return;
-            }
-
-            lock (_timerLock)
-            {
-                if (KillTimer == null)
-                {
-                    Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
-                    KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
-                }
-                else
-                {
-                    Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
-                    KillTimer.Change(intervalMs, Timeout.Infinite);
-                }
-            }
-        }
-
-        public void ChangeKillTimerIfStarted()
-        {
-            if (HasExited)
-            {
-                return;
-            }
-
-            lock (_timerLock)
-            {
-                if (KillTimer != null)
-                {
-                    var intervalMs = PingTimeout;
-
-                    Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
-                    KillTimer.Change(intervalMs, Timeout.Infinite);
-                }
-            }
-        }
-    }
-}

From dee7bdddb6f5ce0cc09dc2b20d4dab9747eea9f0 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Mon, 3 Aug 2020 14:49:24 -0600
Subject: [PATCH 424/463] fix build

---
 Emby.Server.Implementations/ApplicationHost.cs | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7647827fb6..0201ed7a34 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -46,7 +46,6 @@ using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Jellyfin.Api.Helpers;
-using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
@@ -1032,9 +1031,6 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            // Include composable parts in the Api assembly
-            yield return typeof(ApiEntryPoint).Assembly;
-
             // Include composable parts in the Model assembly
             yield return typeof(SystemInfo).Assembly;
 

From 52d409101bb0faf3f8cc5964bf9a4d4aa4c1cd7b Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Tue, 4 Aug 2020 09:05:51 +0200
Subject: [PATCH 425/463] Change OnRefreshStart and OnRefreshComplete logging
 levels to debug

---
 MediaBrowser.Providers/Manager/ProviderManager.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 9170c70025..2d6935a0b4 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -966,7 +966,7 @@ namespace MediaBrowser.Providers.Manager
         /// <inheritdoc/>
         public void OnRefreshStart(BaseItem item)
         {
-            _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+            _logger.LogDebug("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
             _activeRefreshes[item.Id] = 0;
             RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
@@ -974,7 +974,7 @@ namespace MediaBrowser.Providers.Manager
         /// <inheritdoc/>
         public void OnRefreshComplete(BaseItem item)
         {
-            _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+            _logger.LogDebug("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
 
             _activeRefreshes.Remove(item.Id, out _);
 

From 18efa25a6fdcfab2326cb35bb5781138a83e9d56 Mon Sep 17 00:00:00 2001
From: Bond-009 <bond.009@outlook.com>
Date: Tue, 4 Aug 2020 16:20:52 +0200
Subject: [PATCH 426/463] Enable TreatWarningsAsErrors for
 MediaBrowser.MediaEncoding

---
 .../Attachments/AttachmentExtractor.cs                      | 2 ++
 MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs         | 4 ++++
 .../Configuration/EncodingConfigurationFactory.cs           | 2 ++
 MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs      | 2 ++
 MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs         | 2 ++
 MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs          | 2 ++
 .../MediaBrowser.MediaEncoding.csproj                       | 1 +
 MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs        | 3 +++
 MediaBrowser.MediaEncoding/Probing/MediaChapter.cs          | 2 ++
 MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs       | 4 ++++
 MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 2 ++
 MediaBrowser.MediaEncoding/Subtitles/AssParser.cs           | 3 +++
 MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs     | 2 ++
 MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs     | 6 +++---
 14 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index f029993706..a8ebe6bc54 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Concurrent;
 using System.Diagnostics;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
index ccfae2fa54..9108d96497 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -15,6 +15,10 @@ namespace MediaBrowser.MediaEncoding.BdInfo
     {
         private readonly IFileSystem _fileSystem;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
+        /// </summary>
+        /// <param name="fileSystem">The filesystem.</param>
         public BdInfoExaminer(IFileSystem fileSystem)
         {
             _fileSystem = fileSystem;
diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
index 75534b5bdd..fea7ee6fed 100644
--- a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
+++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 5c43fdcfa9..1ac56f845f 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index d4aede572b..7c2d9f1fd5 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Linq;
 using MediaBrowser.Model.MediaInfo;
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 62fdbc618a..0f0ae877fb 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index aeb4dbe733..dab5f866cf 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -9,6 +9,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 3aa296f7f5..b2d4db894d 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -3,6 +3,9 @@ using System.Collections.Generic;
 
 namespace MediaBrowser.MediaEncoding.Probing
 {
+    /// <summary>
+    /// Class containing helper methods for working with FFprobe output.
+    /// </summary>
     public static class FFProbeHelpers
     {
         /// <summary>
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
index 6a45ccf495..de062d06b0 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
 
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index a2ea0766a8..93ef6f93e1 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -269,6 +269,10 @@ namespace MediaBrowser.MediaEncoding.Probing
         [JsonPropertyName("loro_surmixlev")]
         public string LoroSurmixlev { get; set; }
 
+        /// <summary>
+        /// Gets or sets the field_order.
+        /// </summary>
+        /// <value>The loro_surmixlev.</value>
         [JsonPropertyName("field_order")]
         public string FieldOrder { get; set; }
 
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 0b447e3e64..8aaaf4a09b 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index 43a45291cc..e6e21756ad 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -13,6 +15,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
+        /// <inheritdoc />
         public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
         {
             var trackInfo = new SubtitleTrackInfo();
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
index f0d1071967..c0023ebf24 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 using System.Threading;
 using MediaBrowser.Model.MediaInfo;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 2afa89cdab..7c0697279c 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -380,6 +380,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// Converts the text subtitle to SRT.
         /// </summary>
         /// <param name="inputPath">The input path.</param>
+        /// <param name="language">The language.</param>
         /// <param name="inputProtocol">The input protocol.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -407,14 +408,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// Converts the text subtitle to SRT internal.
         /// </summary>
         /// <param name="inputPath">The input path.</param>
+        /// <param name="language">The language.</param>
         /// <param name="inputProtocol">The input protocol.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <exception cref="ArgumentNullException">
-        /// inputPath
-        /// or
-        /// outputPath
+        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>
         /// </exception>
         private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
         {

From 8f6c2e767906c1d4c62d51ae2e66af1a78edde9a Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 4 Aug 2020 08:27:54 -0600
Subject: [PATCH 427/463] Remove leading slash from route attributes

---
 .../Services/SwaggerService.cs                | 287 ------------------
 .../Controllers/ActivityLogController.cs      |   2 +-
 Jellyfin.Api/Controllers/ApiKeyController.cs  |   2 +-
 Jellyfin.Api/Controllers/ArtistsController.cs |   2 +-
 .../Controllers/CollectionController.cs       |   2 +-
 .../Controllers/ItemRefreshController.cs      |   3 +-
 .../Controllers/LibraryStructureController.cs |   2 +-
 Jellyfin.Api/Controllers/SearchController.cs  |   2 +-
 Jellyfin.Api/Controllers/SystemController.cs  |   2 +-
 .../Controllers/TimeSyncController.cs         |   2 +-
 Jellyfin.Api/Controllers/TvShowsController.cs |   2 +-
 Jellyfin.Api/Controllers/UserController.cs    |   2 +-
 12 files changed, 11 insertions(+), 299 deletions(-)
 delete mode 100644 Emby.Server.Implementations/Services/SwaggerService.cs

diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs
deleted file mode 100644
index 4f011a678f..0000000000
--- a/Emby.Server.Implementations/Services/SwaggerService.cs
+++ /dev/null
@@ -1,287 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
-    [Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
-    [Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
-    public class GetSwaggerSpec : IReturn<SwaggerSpec>
-    {
-    }
-
-    public class SwaggerSpec
-    {
-        public string swagger { get; set; }
-
-        public string[] schemes { get; set; }
-
-        public SwaggerInfo info { get; set; }
-
-        public string host { get; set; }
-
-        public string basePath { get; set; }
-
-        public SwaggerTag[] tags { get; set; }
-
-        public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
-
-        public Dictionary<string, SwaggerDefinition> definitions { get; set; }
-
-        public SwaggerComponents components { get; set; }
-    }
-
-    public class SwaggerComponents
-    {
-        public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
-    }
-
-    public class SwaggerSecurityScheme
-    {
-        public string name { get; set; }
-
-        public string type { get; set; }
-
-        public string @in { get; set; }
-    }
-
-    public class SwaggerInfo
-    {
-        public string description { get; set; }
-
-        public string version { get; set; }
-
-        public string title { get; set; }
-
-        public string termsOfService { get; set; }
-
-        public SwaggerConcactInfo contact { get; set; }
-    }
-
-    public class SwaggerConcactInfo
-    {
-        public string email { get; set; }
-
-        public string name { get; set; }
-
-        public string url { get; set; }
-    }
-
-    public class SwaggerTag
-    {
-        public string description { get; set; }
-
-        public string name { get; set; }
-    }
-
-    public class SwaggerMethod
-    {
-        public string summary { get; set; }
-
-        public string description { get; set; }
-
-        public string[] tags { get; set; }
-
-        public string operationId { get; set; }
-
-        public string[] consumes { get; set; }
-
-        public string[] produces { get; set; }
-
-        public SwaggerParam[] parameters { get; set; }
-
-        public Dictionary<string, SwaggerResponse> responses { get; set; }
-
-        public Dictionary<string, string[]>[] security { get; set; }
-    }
-
-    public class SwaggerParam
-    {
-        public string @in { get; set; }
-
-        public string name { get; set; }
-
-        public string description { get; set; }
-
-        public bool required { get; set; }
-
-        public string type { get; set; }
-
-        public string collectionFormat { get; set; }
-    }
-
-    public class SwaggerResponse
-    {
-        public string description { get; set; }
-
-        // ex. "$ref":"#/definitions/Pet"
-        public Dictionary<string, string> schema { get; set; }
-    }
-
-    public class SwaggerDefinition
-    {
-        public string type { get; set; }
-
-        public Dictionary<string, SwaggerProperty> properties { get; set; }
-    }
-
-    public class SwaggerProperty
-    {
-        public string type { get; set; }
-
-        public string format { get; set; }
-
-        public string description { get; set; }
-
-        public string[] @enum { get; set; }
-
-        public string @default { get; set; }
-    }
-
-    public class SwaggerService : IService, IRequiresRequest
-    {
-        private readonly IHttpServer _httpServer;
-        private SwaggerSpec _spec;
-
-        public IRequest Request { get; set; }
-
-        public SwaggerService(IHttpServer httpServer)
-        {
-            _httpServer = httpServer;
-        }
-
-        public object Get(GetSwaggerSpec request)
-        {
-            return _spec ?? (_spec = GetSpec());
-        }
-
-        private SwaggerSpec GetSpec()
-        {
-            string host = null;
-            Uri uri;
-            if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
-            {
-                host = uri.Host;
-            }
-
-            var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
-
-            securitySchemes["api_key"] = new SwaggerSecurityScheme
-            {
-                name = "api_key",
-                type = "apiKey",
-                @in = "query"
-            };
-
-            var spec = new SwaggerSpec
-            {
-                schemes = new[] { "http" },
-                tags = GetTags(),
-                swagger = "2.0",
-                info = new SwaggerInfo
-                {
-                    title = "Jellyfin Server API",
-                    version = "1.0.0",
-                    description = "Explore the Jellyfin Server API",
-                    contact = new SwaggerConcactInfo
-                    {
-                        name = "Jellyfin Community",
-                        url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
-                    }
-                },
-                paths = GetPaths(),
-                definitions = GetDefinitions(),
-                basePath = "/jellyfin",
-                host = host,
-
-                components = new SwaggerComponents
-                {
-                    securitySchemes = securitySchemes
-                }
-            };
-
-            return spec;
-        }
-
-
-        private SwaggerTag[] GetTags()
-        {
-            return Array.Empty<SwaggerTag>();
-        }
-
-        private Dictionary<string, SwaggerDefinition> GetDefinitions()
-        {
-            return new Dictionary<string, SwaggerDefinition>();
-        }
-
-        private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
-        {
-            var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
-
-            // REVIEW: this can be done better
-            var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
-
-            foreach (var current in all)
-            {
-                foreach (var info in current.Value)
-                {
-                    if (info.IsHidden)
-                    {
-                        continue;
-                    }
-
-                    if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
-                        || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
-                    {
-                        continue;
-                    }
-
-                    paths[info.Path] = GetPathInfo(info);
-                }
-            }
-
-            return paths;
-        }
-
-        private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
-        {
-            var result = new Dictionary<string, SwaggerMethod>();
-
-            foreach (var verb in info.Verbs)
-            {
-                var responses = new Dictionary<string, SwaggerResponse>
-                {
-                    { "200", new SwaggerResponse { description = "OK" } }
-                };
-
-                var apiKeySecurity = new Dictionary<string, string[]>
-                {
-                    { "api_key",  Array.Empty<string>() }
-                };
-
-                result[verb.ToLowerInvariant()] = new SwaggerMethod
-                {
-                    summary = info.Summary,
-                    description = info.Description,
-                    produces = new[] { "application/json" },
-                    consumes = new[] { "application/json" },
-                    operationId = info.RequestType.Name,
-                    tags = Array.Empty<string>(),
-
-                    parameters = Array.Empty<SwaggerParam>(),
-
-                    responses = responses,
-
-                    security = new[] { apiKeySecurity }
-                };
-            }
-
-            return result;
-        }
-    }
-}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 12ea249731..a07cea9c0c 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Activity log controller.
     /// </summary>
-    [Route("/System/ActivityLog")]
+    [Route("System/ActivityLog")]
     [Authorize(Policy = Policies.RequiresElevation)]
     public class ActivityLogController : BaseJellyfinApiController
     {
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index fef4d7262d..ccb7f47f08 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Authentication controller.
     /// </summary>
-    [Route("/Auth")]
+    [Route("Auth")]
     public class ApiKeyController : BaseJellyfinApiController
     {
         private readonly ISessionManager _sessionManager;
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index d390214465..9d10108036 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// The artists controller.
     /// </summary>
     [Authorize(Policy = Policies.DefaultAuthorization)]
-    [Route("/Artists")]
+    [Route("Artists")]
     public class ArtistsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 6f78a7d844..1e8426df4e 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// The collection controller.
     /// </summary>
     [Authorize(Policy = Policies.DefaultAuthorization)]
-    [Route("/Collections")]
+    [Route("Collections")]
     public class CollectionController : BaseJellyfinApiController
     {
         private readonly ICollectionManager _collectionManager;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 4697d869dc..3f5d305c18 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -13,8 +13,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Item Refresh Controller.
     /// </summary>
-    /// [Authenticated]
-    [Route("/Items")]
+    [Route("Items")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ItemRefreshController : BaseJellyfinApiController
     {
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 827879e0a8..ca150f3f24 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -24,7 +24,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The library structure controller.
     /// </summary>
-    [Route("/Library/VirtualFolders")]
+    [Route("Library/VirtualFolders")]
     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
     public class LibraryStructureController : BaseJellyfinApiController
     {
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 2cbd32d2f7..e159a96666 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Search controller.
     /// </summary>
-    [Route("/Search/Hints")]
+    [Route("Search/Hints")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class SearchController : BaseJellyfinApiController
     {
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index e0bce3a417..6f9a75e2f5 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The system controller.
     /// </summary>
-    [Route("/System")]
+    [Route("System")]
     public class SystemController : BaseJellyfinApiController
     {
         private readonly IServerApplicationHost _appHost;
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 57a720b26c..bbabcd6e60 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The time sync controller.
     /// </summary>
-    [Route("/GetUtcTime")]
+    [Route("GetUtcTime")]
     public class TimeSyncController : BaseJellyfinApiController
     {
         /// <summary>
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 508b5e24e4..d4560dfa25 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The tv shows controller.
     /// </summary>
-    [Route("/Shows")]
+    [Route("Shows")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class TvShowsController : BaseJellyfinApiController
     {
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index ce0c9281b9..482baf6416 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -27,7 +27,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// User controller.
     /// </summary>
-    [Route("/Users")]
+    [Route("Users")]
     public class UserController : BaseJellyfinApiController
     {
         private readonly IUserManager _userManager;

From e1226f12f2630b914ad8fe16d9037fb175e8196b Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 4 Aug 2020 08:30:03 -0600
Subject: [PATCH 428/463] fix attribute ordering

---
 Jellyfin.Api/Controllers/ArtistsController.cs    | 2 +-
 Jellyfin.Api/Controllers/CollectionController.cs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 9d10108036..3f72830cdd 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -18,8 +18,8 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The artists controller.
     /// </summary>
-    [Authorize(Policy = Policies.DefaultAuthorization)]
     [Route("Artists")]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ArtistsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 1e8426df4e..b63fc5ab19 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -15,8 +15,8 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The collection controller.
     /// </summary>
-    [Authorize(Policy = Policies.DefaultAuthorization)]
     [Route("Collections")]
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class CollectionController : BaseJellyfinApiController
     {
         private readonly ICollectionManager _collectionManager;

From 2b7cefdf15cf15a3e2e9f8499af7a008ea4a9cca Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 4 Aug 2020 08:42:09 -0600
Subject: [PATCH 429/463] Remove references to MediaBrowser.Api

---
 Emby.Server.Implementations/Emby.Server.Implementations.csproj | 1 -
 tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj             | 2 --
 tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj     | 1 -
 3 files changed, 4 deletions(-)

diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index a1103b051f..d3e212be13 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -15,7 +15,6 @@
     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
     <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
     <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
     <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 05bb368323..4011e4aa8c 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -33,8 +33,6 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
-    <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
     <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
   </ItemGroup>
 
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
index 8309faebd1..93bc8433af 100644
--- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -17,7 +17,6 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
-    <ProjectReference Include="..\..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
   </ItemGroup>
 
   <!-- Code Analyzers -->

From 53f99d5d4bdf3f2f5b65d53f9d84f1ea220e58e7 Mon Sep 17 00:00:00 2001
From: Bond-009 <bond.009@outlook.com>
Date: Tue, 4 Aug 2020 17:08:09 +0200
Subject: [PATCH 430/463] Add some analyzers to MediaBrowser.MediaEncoding

---
 .../BdInfo/BdInfoDirectoryInfo.cs             | 19 +++--
 .../BdInfo/BdInfoFileInfo.cs                  | 19 +++--
 .../EncodingConfigurationFactory.cs           | 32 --------
 .../EncodingConfigurationStore.cs             | 38 ++++++++++
 .../Encoder/EncoderValidator.cs               | 30 ++++----
 .../Encoder/MediaEncoder.cs                   | 74 +++++++++++--------
 .../MediaBrowser.MediaEncoding.csproj         | 12 +++
 .../Probing/ProbeResultNormalizer.cs          | 65 ++++++++--------
 .../Subtitles/AssParser.cs                    | 18 ++---
 .../Subtitles/SsaParser.cs                    | 14 ++--
 .../Subtitles/SubtitleEncoder.cs              | 58 +++++++--------
 11 files changed, 210 insertions(+), 169 deletions(-)
 create mode 100644 MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs

diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index e040286ab9..4a54b677dd 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Linq;
 using BDInfo.IO;
@@ -5,7 +7,7 @@ using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.MediaEncoding.BdInfo
 {
-    class BdInfoDirectoryInfo : IDirectoryInfo
+    public class BdInfoDirectoryInfo : IDirectoryInfo
     {
         private readonly IFileSystem _fileSystem = null;
 
@@ -43,25 +45,32 @@ namespace MediaBrowser.MediaEncoding.BdInfo
 
         public IDirectoryInfo[] GetDirectories()
         {
-            return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(),
+            return Array.ConvertAll(
+                _fileSystem.GetDirectories(_impl.FullName).ToArray(),
                 x => new BdInfoDirectoryInfo(_fileSystem, x));
         }
 
         public IFileInfo[] GetFiles()
         {
-            return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(),
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(_impl.FullName).ToArray(),
                 x => new BdInfoFileInfo(x));
         }
 
         public IFileInfo[] GetFiles(string searchPattern)
         {
-            return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
                 x => new BdInfoFileInfo(x));
         }
 
         public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
         {
-            return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false,
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(
+                    _impl.FullName,
+                    new[] { searchPattern },
+                    false,
                     searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(),
                 x => new BdInfoFileInfo(x));
         }
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index a6ff4f7678..0a8af8e9c8 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -1,11 +1,18 @@
+#pragma warning disable CS1591
+
 using System.IO;
 using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.MediaEncoding.BdInfo
 {
-    class BdInfoFileInfo : BDInfo.IO.IFileInfo
+    public class BdInfoFileInfo : BDInfo.IO.IFileInfo
     {
-        FileSystemMetadata _impl = null;
+        private FileSystemMetadata _impl = null;
+
+        public BdInfoFileInfo(FileSystemMetadata impl)
+        {
+            _impl = impl;
+        }
 
         public string Name => _impl.Name;
 
@@ -17,14 +24,10 @@ namespace MediaBrowser.MediaEncoding.BdInfo
 
         public bool IsDir => _impl.IsDirectory;
 
-        public BdInfoFileInfo(FileSystemMetadata impl)
-        {
-            _impl = impl;
-        }
-
         public System.IO.Stream OpenRead()
         {
-            return new FileStream(FullName,
+            return new FileStream(
+                FullName,
                 FileMode.Open,
                 FileAccess.Read,
                 FileShare.Read);
diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
index fea7ee6fed..f81a337dbb 100644
--- a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
+++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationFactory.cs
@@ -1,11 +1,7 @@
 #pragma warning disable CS1591
 
-using System;
 using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.MediaEncoding.Configuration
 {
@@ -19,32 +15,4 @@ namespace MediaBrowser.MediaEncoding.Configuration
             };
         }
     }
-
-    public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
-    {
-        public EncodingConfigurationStore()
-        {
-            ConfigurationType = typeof(EncodingOptions);
-            Key = "encoding";
-        }
-
-        public void Validate(object oldConfig, object newConfig)
-        {
-            var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
-
-            if (!string.IsNullOrWhiteSpace(newPath)
-                && !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
-            {
-                // Validate
-                if (!Directory.Exists(newPath))
-                {
-                    throw new DirectoryNotFoundException(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "{0} does not exist.",
-                            newPath));
-                }
-            }
-        }
-    }
 }
diff --git a/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs
new file mode 100644
index 0000000000..2f158157e8
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Configuration/EncodingConfigurationStore.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.MediaEncoding.Configuration
+{
+    public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
+    {
+        public EncodingConfigurationStore()
+        {
+            ConfigurationType = typeof(EncodingOptions);
+            Key = "encoding";
+        }
+
+        public void Validate(object oldConfig, object newConfig)
+        {
+            var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
+
+            if (!string.IsNullOrWhiteSpace(newPath)
+                && !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
+            {
+                // Validate
+                if (!Directory.Exists(newPath))
+                {
+                    throw new DirectoryNotFoundException(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "{0} does not exist.",
+                            newPath));
+                }
+            }
+        }
+    }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 1ac56f845f..75123a843e 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -14,7 +14,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
     {
         private const string DefaultEncoderPath = "ffmpeg";
 
-        private static readonly string[] requiredDecoders = new[]
+        private static readonly string[] _requiredDecoders = new[]
         {
             "h264",
             "hevc",
@@ -57,7 +57,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             "vc1_opencl"
         };
 
-        private static readonly string[] requiredEncoders = new[]
+        private static readonly string[] _requiredEncoders = new[]
         {
             "libx264",
             "libx265",
@@ -112,6 +112,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _encoderPath = encoderPath;
         }
 
+        private enum Codec
+        {
+            Encoder,
+            Decoder
+        }
+
         public static Version MinVersion { get; } = new Version(4, 0);
 
         public static Version MaxVersion { get; } = null;
@@ -195,8 +201,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// If that fails then we use one of the main libraries to determine if it's new/older than the latest
         /// we have stored.
         /// </summary>
-        /// <param name="output"></param>
-        /// <returns></returns>
+        /// <param name="output">The output from "ffmpeg -version".</param>
+        /// <returns>The FFmpeg version.</returns>
         internal static Version GetFFmpegVersion(string output)
         {
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
@@ -218,10 +224,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         /// <summary>
         /// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
-        /// and condenses them on to one line.  Output format is "name1=major.minor,name2=major.minor,etc."
+        /// and condenses them on to one line.  Output format is "name1=major.minor,name2=major.minor,etc.".
         /// </summary>
-        /// <param name="output"></param>
-        /// <returns></returns>
+        /// <param name="output">The 'ffmpeg -version' output.</param>
+        /// <returns>The library names and major.minor version numbers.</returns>
         private static string GetLibrariesVersionString(string output)
         {
             var rc = new StringBuilder(144);
@@ -241,12 +247,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return rc.Length == 0 ? null : rc.ToString();
         }
 
-        private enum Codec
-        {
-            Encoder,
-            Decoder
-        }
-
         private IEnumerable<string> GetHwaccelTypes()
         {
             string output = null;
@@ -264,7 +264,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 return Enumerable.Empty<string>();
             }
 
-            var found = output.Split(new char[] {'\r','\n'}, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
+            var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
             _logger.LogInformation("Available hwaccel types: {Types}", found);
 
             return found;
@@ -288,7 +288,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 return Enumerable.Empty<string>();
             }
 
-            var required = codec == Codec.Encoder ? requiredEncoders : requiredDecoders;
+            var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
 
             var found = Regex
                 .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline)
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 0f0ae877fb..62e6e8e3c9 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -21,9 +22,8 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
-using Microsoft.Extensions.Logging;
-using System.Diagnostics;
 using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.MediaEncoding.Encoder
 {
@@ -37,6 +37,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// </summary>
         internal const int DefaultImageExtractionTimeout = 5000;
 
+        /// <summary>
+        /// The us culture.
+        /// </summary>
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
         private readonly ILogger<MediaEncoder> _logger;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IFileSystem _fileSystem;
@@ -49,6 +54,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private readonly object _runningProcessesLock = new object();
         private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 
+        private List<string> _encoders = new List<string>();
+        private List<string> _decoders = new List<string>();
+        private List<string> _hwaccels = new List<string>();
+
         private string _ffmpegPath = string.Empty;
         private string _ffprobePath;
 
@@ -79,7 +88,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <summary>
         /// Run at startup or if the user removes a Custom path from transcode page.
         /// Sets global variables FFmpegPath.
-        /// Precedence is: Config > CLI > $PATH
+        /// Precedence is: Config > CLI > $PATH.
         /// </summary>
         public void SetFFmpegPath()
         {
@@ -124,8 +133,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
         /// Only write the new path to xml if it exists.  Do not perform validation checks on ffmpeg here.
         /// </summary>
-        /// <param name="path"></param>
-        /// <param name="pathType"></param>
+        /// <param name="path">The path.</param>
+        /// <param name="pathType">The path type.</param>
         public void UpdateEncoderPath(string path, string pathType)
         {
             string newPath;
@@ -170,8 +179,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
         /// </summary>
         /// <param name="path">FQPN to test.</param>
-        /// <param name="location">Location (External, Custom, System) of tool</param>
-        /// <returns></returns>
+        /// <param name="location">Location (External, Custom, System) of tool.</param>
+        /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
         private bool ValidatePath(string path, FFmpegLocation location)
         {
             bool rc = false;
@@ -223,8 +232,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <summary>
         /// Search the system $PATH environment variable looking for given filename.
         /// </summary>
-        /// <param name="fileName"></param>
-        /// <returns></returns>
+        /// <param name="fileName">The filename.</param>
+        /// <returns>The full path to the file.</returns>
         private string ExistsOnSystemPath(string fileName)
         {
             var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true);
@@ -248,25 +257,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return null;
         }
 
-        private List<string> _encoders = new List<string>();
         public void SetAvailableEncoders(IEnumerable<string> list)
         {
             _encoders = list.ToList();
-            // _logger.Info("Supported encoders: {0}", string.Join(",", list.ToArray()));
         }
 
-        private List<string> _decoders = new List<string>();
         public void SetAvailableDecoders(IEnumerable<string> list)
         {
             _decoders = list.ToList();
-            // _logger.Info("Supported decoders: {0}", string.Join(",", list.ToArray()));
         }
 
-        private List<string> _hwaccels = new List<string>();
         public void SetAvailableHwaccels(IEnumerable<string> list)
         {
             _hwaccels = list.ToList();
-            //_logger.Info("Supported hwaccels: {0}", string.Join(",", list.ToArray()));
         }
 
         public bool SupportsEncoder(string encoder)
@@ -334,8 +337,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
 
-            return GetMediaInfoInternal(GetInputArgument(inputFiles, request.MediaSource.Protocol), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters,
-                probeSize, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, forceEnableLogging, cancellationToken);
+            return GetMediaInfoInternal(
+                GetInputArgument(inputFiles, request.MediaSource.Protocol),
+                request.MediaSource.Path,
+                request.MediaSource.Protocol,
+                extractChapters,
+                probeSize,
+                request.MediaType == DlnaProfileType.Audio,
+                request.MediaSource.VideoType,
+                forceEnableLogging,
+                cancellationToken);
         }
 
         /// <summary>
@@ -344,7 +355,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <param name="inputFiles">The input files.</param>
         /// <param name="protocol">The protocol.</param>
         /// <returns>System.String.</returns>
-        /// <exception cref="ArgumentException">Unrecognized InputType</exception>
+        /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
         public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol)
             => EncodingUtils.GetInputArgument(inputFiles, protocol);
 
@@ -352,7 +363,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// Gets the media info internal.
         /// </summary>
         /// <returns>Task{MediaInfoResult}.</returns>
-        private async Task<MediaInfo> GetMediaInfoInternal(string inputPath,
+        private async Task<MediaInfo> GetMediaInfoInternal(
+            string inputPath,
             string primaryPath,
             MediaProtocol protocol,
             bool extractChapters,
@@ -380,7 +392,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     FileName = _ffprobePath,
                     Arguments = args,
 
-
                     WindowStyle = ProcessWindowStyle.Hidden,
                     ErrorDialog = false,
                 },
@@ -441,11 +452,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
-        /// <summary>
-        /// The us culture.
-        /// </summary>
-        protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
         public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
         {
             return ExtractImage(new[] { path }, null, null, imageStreamIndex, MediaProtocol.File, true, null, null, cancellationToken);
@@ -461,8 +467,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, protocol, false, null, null, cancellationToken);
         }
 
-        private async Task<string> ExtractImage(string[] inputFiles, string container, MediaStream videoStream, int? imageStreamIndex, MediaProtocol protocol, bool isAudio,
-            Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
+        private async Task<string> ExtractImage(
+            string[] inputFiles,
+            string container,
+            MediaStream videoStream,
+            int? imageStreamIndex,
+            MediaProtocol protocol,
+            bool isAudio,
+            Video3DFormat? threedFormat,
+            TimeSpan? offset,
+            CancellationToken cancellationToken)
         {
             var inputArgument = GetInputArgument(inputFiles, protocol);
 
@@ -647,7 +661,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         public string GetTimeParameter(TimeSpan time)
         {
-            return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
+            return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
         }
 
         public async Task ExtractVideoImagesOnInterval(
@@ -664,11 +678,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
             var inputArgument = GetInputArgument(inputFiles, protocol);
 
-            var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
+            var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
 
             if (maxWidth.HasValue)
             {
-                var maxWidthParam = maxWidth.Value.ToString(UsCulture);
+                var maxWidthParam = maxWidth.Value.ToString(_usCulture);
 
                 vf += string.Format(",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
             }
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index dab5f866cf..017f917e27 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -28,4 +28,16 @@
     <PackageReference Include="UTF.Unknown" Version="2.3.0" />
   </ItemGroup>
 
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
+  <!-- Code Analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
 </Project>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 8aaaf4a09b..19e3bd8e60 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -18,10 +18,19 @@ namespace MediaBrowser.MediaEncoding.Probing
 {
     public class ProbeResultNormalizer
     {
+        // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
+        private const int MaxSubtitleDescriptionExtractionLength = 100;
+
+        private const string ArtistReplaceValue = " | ";
+
+        private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
+
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly ILogger _logger;
         private readonly ILocalizationManager _localization;
 
+        private List<string> _splitWhiteList = null;
+
         public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
         {
             _logger = logger;
@@ -370,7 +379,6 @@ namespace MediaBrowser.MediaEncoding.Probing
 
         private List<NameValuePair> ReadValueArray(XmlReader reader)
         {
-
             var pairs = new List<NameValuePair>();
 
             reader.MoveToContent();
@@ -951,50 +959,46 @@ namespace MediaBrowser.MediaEncoding.Probing
 
         private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
         {
+            var peoples = new List<BaseItemPerson>();
             var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
             if (!string.IsNullOrWhiteSpace(composer))
             {
-                var peoples = new List<BaseItemPerson>();
                 foreach (var person in Split(composer, false))
                 {
                     peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
                 }
-
-                audio.People = peoples.ToArray();
             }
 
-            // var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor");
-            // if (!string.IsNullOrWhiteSpace(conductor))
-            //{
-            //    foreach (var person in Split(conductor, false))
-            //    {
-            //        audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
-            //    }
-            //}
+            var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor");
+            if (!string.IsNullOrWhiteSpace(conductor))
+            {
+                foreach (var person in Split(conductor, false))
+                {
+                    peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
+                }
+            }
 
-            // var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
-            // if (!string.IsNullOrWhiteSpace(lyricist))
-            //{
-            //    foreach (var person in Split(lyricist, false))
-            //    {
-            //        audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
-            //    }
-            //}
+            var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
+            if (!string.IsNullOrWhiteSpace(lyricist))
+            {
+                foreach (var person in Split(lyricist, false))
+                {
+                    peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+                }
+            }
 
             // Check for writer some music is tagged that way as alternative to composer/lyricist
             var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
 
             if (!string.IsNullOrWhiteSpace(writer))
             {
-                var peoples = new List<BaseItemPerson>();
                 foreach (var person in Split(writer, false))
                 {
                     peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
                 }
-
-                audio.People = peoples.ToArray();
             }
 
+            audio.People = peoples.ToArray();
             audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
 
             var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
@@ -1119,8 +1123,6 @@ namespace MediaBrowser.MediaEncoding.Probing
                 .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
         }
 
-        private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
-
         /// <summary>
         /// Splits the specified val.
         /// </summary>
@@ -1140,8 +1142,6 @@ namespace MediaBrowser.MediaEncoding.Probing
                 .Select(i => i.Trim());
         }
 
-        private const string ArtistReplaceValue = " | ";
-
         private IEnumerable<string> SplitArtists(string val, char[] delimiters, bool splitFeaturing)
         {
             if (splitFeaturing)
@@ -1171,9 +1171,6 @@ namespace MediaBrowser.MediaEncoding.Probing
             return artistsFound;
         }
 
-
-        private List<string> _splitWhiteList = null;
-
         private IEnumerable<string> GetSplitWhitelist()
         {
             if (_splitWhiteList == null)
@@ -1250,7 +1247,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         }
 
         /// <summary>
-        /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
+        /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'.
         /// </summary>
         /// <param name="tags">The tags.</param>
         /// <param name="tagName">Name of the tag.</param>
@@ -1296,8 +1293,6 @@ namespace MediaBrowser.MediaEncoding.Probing
             return info;
         }
 
-        private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
-
         private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
         {
             if (data.Format == null || data.Format.Tags == null)
@@ -1382,8 +1377,8 @@ namespace MediaBrowser.MediaEncoding.Probing
                         if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
                         {
                             string[] numbers = subtitle.Split(' ');
-                            video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
-                            int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
+                            video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0]);
+                            int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1]);
 
                             description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
                         }
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index e6e21756ad..308b62886c 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -25,7 +25,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
                 string line;
                 while (reader.ReadLine() != "[Events]")
-                { }
+                {
+                }
 
                 var headers = ParseFieldHeaders(reader.ReadLine());
 
@@ -75,17 +76,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         {
             var fields = line.Substring(8).Split(',').Select(x => x.Trim()).ToList();
 
-            var result = new Dictionary<string, int> {
-                                                         {"Start", fields.IndexOf("Start")},
-                                                         {"End", fields.IndexOf("End")},
-                                                         {"Text", fields.IndexOf("Text")}
-                                                     };
-            return result;
+            return new Dictionary<string, int>
+            {
+                { "Start", fields.IndexOf("Start") },
+                { "End", fields.IndexOf("End") },
+                { "Text", fields.IndexOf("Text") }
+            };
         }
 
-        /// <summary>
-        /// Credit: https://github.com/SubtitleEdit/subtitleedit/blob/master/src/Logic/SubtitleFormats/AdvancedSubStationAlpha.cs
-        /// </summary>
         private void RemoteNativeFormatting(SubtitleTrackEvent p)
         {
             int indexOfBegin = p.Text.IndexOf('{');
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index bd330f568b..6b7a81e6eb 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -8,7 +8,7 @@ using MediaBrowser.Model.MediaInfo;
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
     /// <summary>
-    /// Credit to https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs
+    /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>.
     /// </summary>
     public class SsaParser : ISubtitleParser
     {
@@ -179,10 +179,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         {
             // h:mm:ss.cc
             string[] timeCode = time.Split(':', '.');
-            return new TimeSpan(0, int.Parse(timeCode[0]),
-                                int.Parse(timeCode[1]),
-                                int.Parse(timeCode[2]),
-                                int.Parse(timeCode[3]) * 10).Ticks;
+            return new TimeSpan(
+                0,
+                int.Parse(timeCode[0]),
+                int.Parse(timeCode[1]),
+                int.Parse(timeCode[2]),
+                int.Parse(timeCode[3]) * 10).Ticks;
         }
 
         private static string GetFormattedText(string text)
@@ -282,6 +284,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                         {
                             text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
                         }
+
                         int indexOfEndTag = text.IndexOf("{\\c}", start);
                         if (indexOfEndTag > 0)
                         {
@@ -320,6 +323,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                         {
                             text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
                         }
+
                         text += "</font>";
                     }
                 }
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 7c0697279c..374e35b969 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -34,6 +34,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         private readonly IHttpClient _httpClient;
         private readonly IMediaSourceManager _mediaSourceManager;
 
+        /// <summary>
+        /// The _semaphoreLocks.
+        /// </summary>
+        private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
+            new ConcurrentDictionary<string, SemaphoreSlim>();
+
         public SubtitleEncoder(
             ILibraryManager libraryManager,
             ILogger<SubtitleEncoder> logger,
@@ -269,25 +275,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             return new SubtitleInfo(subtitleStream.Path, protocol, currentFormat, true);
         }
 
-        private struct SubtitleInfo
-        {
-            public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
-            {
-                Path = path;
-                Protocol = protocol;
-                Format = format;
-                IsExternal = isExternal;
-            }
-
-            public string Path { get; set; }
-
-            public MediaProtocol Protocol { get; set; }
-
-            public string Format { get; set; }
-
-            public bool IsExternal { get; set; }
-        }
-
         private ISubtitleParser GetReader(string format, bool throwIfMissing)
         {
             if (string.IsNullOrEmpty(format))
@@ -360,12 +347,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             throw new ArgumentException("Unsupported format: " + format);
         }
 
-        /// <summary>
-        /// The _semaphoreLocks.
-        /// </summary>
-        private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
-            new ConcurrentDictionary<string, SemaphoreSlim>();
-
         /// <summary>
         /// Gets the lock.
         /// </summary>
@@ -414,7 +395,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <exception cref="ArgumentNullException">
-        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>
+        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
         /// </exception>
         private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
         {
@@ -438,7 +419,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
                  encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
             {
-                encodingParam = "";
+                encodingParam = string.Empty;
             }
             else if (!string.IsNullOrEmpty(encodingParam))
             {
@@ -540,7 +521,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        /// <exception cref="ArgumentException">Must use inputPath list overload</exception>
+        /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
         private async Task ExtractTextSubtitle(
             string[] inputFiles,
             MediaProtocol protocol,
@@ -759,7 +740,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
                         || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
                 {
-                    charset = "";
+                    charset = string.Empty;
                 }
 
                 _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
@@ -790,5 +771,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     throw new ArgumentOutOfRangeException(nameof(protocol));
             }
         }
+
+        private struct SubtitleInfo
+        {
+            public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
+            {
+                Path = path;
+                Protocol = protocol;
+                Format = format;
+                IsExternal = isExternal;
+            }
+
+            public string Path { get; set; }
+
+            public MediaProtocol Protocol { get; set; }
+
+            public string Format { get; set; }
+
+            public bool IsExternal { get; set; }
+        }
     }
 }

From d9f6953416ace083e7356cdc20ab4a1bd91f09cd Mon Sep 17 00:00:00 2001
From: Bond-009 <bond.009@outlook.com>
Date: Tue, 4 Aug 2020 17:14:07 +0200
Subject: [PATCH 431/463] Minor fixes

---
 MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs    | 1 +
 MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 62e6e8e3c9..778c0b18c2 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -875,6 +875,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             if (dispose)
             {
                 StopProcesses();
+                _thumbnailResourcePool.Dispose();
             }
         }
 
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 93ef6f93e1..8996d3b098 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -272,7 +272,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         /// <summary>
         /// Gets or sets the field_order.
         /// </summary>
-        /// <value>The loro_surmixlev.</value>
+        /// <value>The field_order.</value>
         [JsonPropertyName("field_order")]
         public string FieldOrder { get; set; }
 

From 858aecd409914f6a9ca6f86445ec20c8bacc0b59 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 4 Aug 2020 12:48:53 -0600
Subject: [PATCH 432/463] Fix all route for base url support

---
 Jellyfin.Api/Controllers/AlbumsController.cs  |  5 +-
 .../Controllers/DashboardController.cs        | 11 ++--
 .../Controllers/DynamicHlsController.cs       | 17 +++---
 Jellyfin.Api/Controllers/FilterController.cs  |  5 +-
 .../Controllers/HlsSegmentController.cs       | 11 ++--
 Jellyfin.Api/Controllers/ImageController.cs   | 57 ++++++++++---------
 .../Controllers/InstantMixController.cs       | 15 ++---
 .../Controllers/ItemLookupController.cs       | 25 ++++----
 .../Controllers/ItemUpdateController.cs       |  7 ++-
 Jellyfin.Api/Controllers/ItemsController.cs   |  7 ++-
 Jellyfin.Api/Controllers/LibraryController.cs | 51 +++++++++--------
 .../Controllers/MediaInfoController.cs        | 11 ++--
 Jellyfin.Api/Controllers/PackageController.cs | 14 ++---
 .../Controllers/PlaystateController.cs        | 19 ++++---
 Jellyfin.Api/Controllers/PluginsController.cs |  2 +-
 Jellyfin.Api/Controllers/SessionController.cs | 33 +++++------
 .../Controllers/SubtitleController.cs         | 15 ++---
 .../Controllers/SuggestionsController.cs      |  3 +-
 .../Controllers/TrailersController.cs         |  2 +-
 .../Controllers/UniversalAudioController.cs   |  9 +--
 Jellyfin.Api/Controllers/UserController.cs    |  2 +-
 .../Controllers/UserLibraryController.cs      | 21 +++----
 .../Controllers/UserViewsController.cs        |  5 +-
 .../Controllers/VideoHlsController.cs         |  5 +-
 24 files changed, 186 insertions(+), 166 deletions(-)

diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 01ba7fc326..190d4bd07c 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -17,6 +17,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The albums controller.
     /// </summary>
+    [Route("")]
     public class AlbumsController : BaseJellyfinApiController
     {
         private readonly IUserManager _userManager;
@@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <response code="200">Similar albums returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
-        [HttpGet("/Albums/{albumId}/Similar")]
+        [HttpGet("Albums/{albumId}/Similar")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
             [FromRoute] string albumId,
@@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <response code="200">Similar artists returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
-        [HttpGet("/Artists/{artistId}/Similar")]
+        [HttpGet("Artists/{artistId}/Similar")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
             [FromRoute] string artistId,
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index e033c50d5c..a7bdb24f6d 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The dashboard controller.
     /// </summary>
+    [Route("")]
     public class DashboardController : BaseJellyfinApiController
     {
         private readonly ILogger<DashboardController> _logger;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">ConfigurationPages returned.</response>
         /// <response code="404">Server still loading.</response>
         /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
-        [HttpGet("/web/ConfigurationPages")]
+        [HttpGet("web/ConfigurationPages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
@@ -118,7 +119,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">ConfigurationPage returned.</response>
         /// <response code="404">Plugin configuration page not found.</response>
         /// <returns>The configuration page.</returns>
-        [HttpGet("/web/ConfigurationPage")]
+        [HttpGet("web/ConfigurationPage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
@@ -172,7 +173,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Robots.txt returned.</response>
         /// <returns>The robots.txt.</returns>
-        [HttpGet("/robots.txt")]
+        [HttpGet("robots.txt")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ApiExplorerSettings(IgnoreApi = true)]
         public ActionResult GetRobotsTxt()
@@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Web client returned.</response>
         /// <response code="404">Server does not host a web client.</response>
         /// <returns>The resource.</returns>
-        [HttpGet("/web/{*resourceName}")]
+        [HttpGet("web/{*resourceName}")]
         [ApiExplorerSettings(IgnoreApi = true)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -218,7 +219,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Favicon.ico returned.</response>
         /// <returns>The favicon.</returns>
-        [HttpGet("/favicon.ico")]
+        [HttpGet("favicon.ico")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ApiExplorerSettings(IgnoreApi = true)]
         public ActionResult GetFavIcon()
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index c4f79ce950..ec65cb95a2 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -37,6 +37,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Dynamic hls controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DynamicHlsController : BaseJellyfinApiController
     {
@@ -164,8 +165,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
-        [HttpGet("/Videos/{itemId}/master.m3u8")]
-        [HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
+        [HttpGet("Videos/{itemId}/master.m3u8")]
+        [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetMasterHlsVideoPlaylist(
             [FromRoute] Guid itemId,
@@ -334,8 +335,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
         /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
-        [HttpGet("/Audio/{itemId}/master.m3u8")]
-        [HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
+        [HttpGet("Audio/{itemId}/master.m3u8")]
+        [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetMasterHlsAudioPlaylist(
             [FromRoute] Guid itemId,
@@ -503,7 +504,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("/Videos/{itemId}/main.m3u8")]
+        [HttpGet("Videos/{itemId}/main.m3u8")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetVariantHlsVideoPlaylist(
             [FromRoute] Guid itemId,
@@ -668,7 +669,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("/Audio/{itemId}/main.m3u8")]
+        [HttpGet("Audio/{itemId}/main.m3u8")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetVariantHlsAudioPlaylist(
             [FromRoute] Guid itemId,
@@ -835,7 +836,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+        [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> GetHlsVideoSegment(
@@ -1004,7 +1005,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("/Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+        [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> GetHlsAudioSegment(
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 9ba5e11618..2a567c8461 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -18,6 +18,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Filters controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class FilterController : BaseJellyfinApiController
     {
@@ -44,7 +45,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
         /// <response code="200">Legacy filters retrieved.</response>
         /// <returns>Legacy query filters.</returns>
-        [HttpGet("/Items/Filters")]
+        [HttpGet("Items/Filters")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
             [FromQuery] Guid? userId,
@@ -133,7 +134,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="recursive">Optional. Search recursive.</param>
         /// <response code="200">Filters retrieved.</response>
         /// <returns>Query filters.</returns>
-        [HttpGet("/Items/Filters2")]
+        [HttpGet("Items/Filters2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryFilters> GetQueryFilters(
             [FromQuery] Guid? userId,
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 7bf9326a71..e4a6842bce 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The hls segment controller.
     /// </summary>
+    [Route("")]
     public class HlsSegmentController : BaseJellyfinApiController
     {
         private readonly IFileSystem _fileSystem;
@@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
         // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
         // [Authenticated]
-        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
-        [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
+        [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
+        [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
         public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
@@ -70,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playlistId">The playlist id.</param>
         /// <response code="200">Hls video playlist returned.</response>
         /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
-        [HttpGet("/Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
+        [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
@@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playSessionId">The play session id.</param>
         /// <response code="204">Encoding stopped successfully.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpDelete("/Videos/ActiveEncodings")]
+        [HttpDelete("Videos/ActiveEncodings")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
@@ -109,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
         // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
         // [Authenticated]
-        [HttpGet("/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
+        [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
         public ActionResult GetHlsVideoSegmentLegacy(
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 3a445b1b3c..360164ad4f 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Image controller.
     /// </summary>
+    [Route("")]
     public class ImageController : BaseJellyfinApiController
     {
         private readonly IUserManager _userManager;
@@ -81,8 +82,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image updated.</response>
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Users/{userId}/Images/{imageType}")]
-        [HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
+        [HttpPost("Users/{userId}/Images/{imageType}")]
+        [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -127,8 +128,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image deleted.</response>
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Users/{userId}/Images/{itemType}")]
-        [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+        [HttpDelete("Users/{userId}/Images/{itemType}")]
+        [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -166,8 +167,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image deleted.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
-        [HttpDelete("/Items/{itemId}/Images/{imageType}")]
-        [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
+        [HttpDelete("Items/{itemId}/Images/{imageType}")]
+        [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -195,8 +196,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image saved.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
-        [HttpPost("/Items/{itemId}/Images/{imageType}")]
-        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
+        [HttpPost("Items/{itemId}/Images/{imageType}")]
+        [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -230,7 +231,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image index updated.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
-        [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
+        [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -257,7 +258,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Item images returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
-        [HttpGet("/Items/{itemId}/Images")]
+        [HttpGet("Items/{itemId}/Images")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
@@ -341,10 +342,10 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Items/{itemId}/Images/{imageType}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
-        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
+        [HttpGet("Items/{itemId}/Images/{imageType}")]
+        [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
+        [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
+        [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetItemImage(
@@ -421,8 +422,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
-        [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
+        [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+        [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetItemImage2(
@@ -499,8 +500,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
+        [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetArtistImage(
@@ -577,8 +578,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
+        [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetGenreImage(
@@ -655,8 +656,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
+        [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetMusicGenreImage(
@@ -733,8 +734,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
+        [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetPersonImage(
@@ -811,8 +812,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
+        [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetStudioImage(
@@ -889,8 +890,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
+        [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
+        [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetUserImage(
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index bb980af3e7..8ca232ceff 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The instant mix controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class InstantMixController : BaseJellyfinApiController
     {
@@ -59,7 +60,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/Songs/{id}/InstantMix")]
+        [HttpGet("Songs/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
             [FromRoute] Guid id,
@@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/Albums/{id}/InstantMix")]
+        [HttpGet("Albums/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
             [FromRoute] Guid id,
@@ -133,7 +134,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/Playlists/{id}/InstantMix")]
+        [HttpGet("Playlists/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
             [FromRoute] Guid id,
@@ -170,7 +171,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/MusicGenres/{name}/InstantMix")]
+        [HttpGet("MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
             [FromRoute] string? name,
@@ -206,7 +207,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/Artists/InstantMix")]
+        [HttpGet("Artists/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
             [FromRoute] Guid id,
@@ -243,7 +244,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/MusicGenres/InstantMix")]
+        [HttpGet("MusicGenres/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
             [FromRoute] Guid id,
@@ -280,7 +281,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("/Items/{id}/InstantMix")]
+        [HttpGet("Items/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
             [FromRoute] Guid id,
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 44709d0ee6..0d9dffbfe6 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Item lookup controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ItemLookupController : BaseJellyfinApiController
     {
@@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">External id info retrieved.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>List of external id info.</returns>
-        [HttpGet("/Items/{itemId}/ExternalIdInfos")]
+        [HttpGet("Items/{itemId}/ExternalIdInfos")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -92,7 +93,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Movie")]
+        [HttpPost("Items/RemoteSearch/Movie")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MovieInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
@@ -109,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Trailer")]
+        [HttpPost("Items/RemoteSearch/Trailer")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<TrailerInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/MusicVideo")]
+        [HttpPost("Items/RemoteSearch/MusicVideo")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MusicVideoInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
@@ -143,7 +144,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Series")]
+        [HttpPost("Items/RemoteSearch/Series")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<SeriesInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
@@ -160,7 +161,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/BoxSet")]
+        [HttpPost("Items/RemoteSearch/BoxSet")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BoxSetInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
@@ -177,7 +178,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/MusicArtist")]
+        [HttpPost("Items/RemoteSearch/MusicArtist")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<ArtistInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
@@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/MusicAlbum")]
+        [HttpPost("Items/RemoteSearch/MusicAlbum")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<AlbumInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
@@ -211,7 +212,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Person")]
+        [HttpPost("Items/RemoteSearch/Person")]
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<PersonLookupInfo> query)
         {
@@ -229,7 +230,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Book")]
+        [HttpPost("Items/RemoteSearch/Book")]
         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BookInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
@@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
         /// </returns>
-        [HttpGet("/Items/RemoteSearch/Image")]
+        [HttpGet("Items/RemoteSearch/Image")]
         public async Task<ActionResult> GetRemoteSearchImage(
             [FromQuery, Required] string imageUrl,
             [FromQuery, Required] string providerName)
@@ -291,7 +292,7 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
         /// The task result contains an <see cref="NoContentResult"/>.
         /// </returns>
-        [HttpPost("/Items/RemoteSearch/Apply/{id}")]
+        [HttpPost("Items/RemoteSearch/Apply/{id}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> ApplySearchCriteria(
             [FromRoute] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index c9b2aafcca..a5d9d36a33 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -24,6 +24,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Item update controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.RequiresElevation)]
     public class ItemUpdateController : BaseJellyfinApiController
     {
@@ -63,7 +64,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Item updated.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
-        [HttpPost("/Items/{itemId}")]
+        [HttpPost("Items/{itemId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request)
@@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Item metadata editor returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
-        [HttpGet("/Items/{itemId}/MetadataEditor")]
+        [HttpGet("Items/{itemId}/MetadataEditor")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId)
@@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Item content type updated.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
-        [HttpPost("/Items/{itemId}/ContentType")]
+        [HttpPost("Items/{itemId}/ContentType")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 354741ced1..1b8b68313b 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -23,6 +23,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The items controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class ItemsController : BaseJellyfinApiController
     {
@@ -139,8 +140,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
         /// <param name="enableImages">Optional, include image information in output.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
-        [HttpGet("/Items")]
-        [HttpGet("/Users/{uId}/Items", Name = "GetItems_2")]
+        [HttpGet("Items")]
+        [HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
             [FromRoute] Guid? uId,
@@ -523,7 +524,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <response code="200">Items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
-        [HttpGet("/Users/{userId}/Items/Resume")]
+        [HttpGet("Users/{userId}/Items/Resume")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
             [FromRoute] Guid userId,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 0ec7e2b8c0..a4637752d1 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -43,6 +43,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Library Controller.
     /// </summary>
+    [Route("")]
     public class LibraryController : BaseJellyfinApiController
     {
         private readonly IProviderManager _providerManager;
@@ -100,7 +101,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">File stream returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
-        [HttpGet("/Items/{itemId}/File")]
+        [HttpGet("Items/{itemId}/File")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Critic reviews returned.</response>
         /// <returns>The list of critic reviews.</returns>
-        [HttpGet("/Items/{itemId}/CriticReviews")]
+        [HttpGet("Items/{itemId}/CriticReviews")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Obsolete("This endpoint is obsolete.")]
         [ProducesResponseType(StatusCodes.Status200OK)]
@@ -139,7 +140,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme songs.</returns>
-        [HttpGet("/Items/{itemId}/ThemeSongs")]
+        [HttpGet("Items/{itemId}/ThemeSongs")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -205,7 +206,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
-        [HttpGet("/Items/{itemId}/ThemeVideos")]
+        [HttpGet("Items/{itemId}/ThemeVideos")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -271,7 +272,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Theme songs and videos returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>The item theme videos.</returns>
-        [HttpGet("/Items/{itemId}/ThemeMedia")]
+        [HttpGet("Items/{itemId}/ThemeMedia")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<AllThemeMediaResult> GetThemeMedia(
@@ -302,7 +303,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="204">Library scan started.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpGet("/Library/Refresh")]
+        [HttpGet("Library/Refresh")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RefreshLibrary()
@@ -326,7 +327,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Item deleted.</response>
         /// <response code="401">Unauthorized access.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Items/{itemId}")]
+        [HttpDelete("Items/{itemId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -356,7 +357,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Items deleted.</response>
         /// <response code="401">Unauthorized access.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Items")]
+        [HttpDelete("Items")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -400,7 +401,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
         /// <response code="200">Item counts returned.</response>
         /// <returns>Item counts.</returns>
-        [HttpGet("/Items/Counts")]
+        [HttpGet("Items/Counts")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ItemCounts> GetItemCounts(
@@ -434,7 +435,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Item parents returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>Item parents.</returns>
-        [HttpGet("/Items/{itemId}/Ancestors")]
+        [HttpGet("Items/{itemId}/Ancestors")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -476,7 +477,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Physical paths returned.</response>
         /// <returns>List of physical paths.</returns>
-        [HttpGet("/Library/PhysicalPaths")]
+        [HttpGet("Library/PhysicalPaths")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<string>> GetPhysicalPaths()
@@ -491,7 +492,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
         /// <response code="200">Media folders returned.</response>
         /// <returns>List of user media folders.</returns>
-        [HttpGet("/Library/MediaFolders")]
+        [HttpGet("Library/MediaFolders")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
@@ -521,8 +522,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="tvdbId">The tvdbId.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Library/Series/Added", Name = "PostAddedSeries")]
-        [HttpPost("/Library/Series/Updated")]
+        [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
+        [HttpPost("Library/Series/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
@@ -551,8 +552,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imdbId">The imdbId.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")]
-        [HttpPost("/Library/Movies/Updated")]
+        [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
+        [HttpPost("Library/Movies/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId)
@@ -593,7 +594,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="updates">A list of updated media paths.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Library/Media/Updated")]
+        [HttpPost("Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates)
@@ -614,7 +615,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
         /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
-        [HttpGet("/Items/{itemId}/Download")]
+        [HttpGet("Items/{itemId}/Download")]
         [Authorize(Policy = Policies.Download)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -679,12 +680,12 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
         /// <response code="200">Similar items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
-        [HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
-        [HttpGet("/Items/{itemId}/Similar")]
-        [HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
-        [HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
-        [HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
-        [HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
+        [HttpGet("Items/{itemId}/Similar")]
+        [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
+        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
+        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
+        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
@@ -735,7 +736,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isNewLibrary">Whether this is a new library.</param>
         /// <response code="200">Library options info returned.</response>
         /// <returns>Library options info.</returns>
-        [HttpGet("/Libraries/AvailableOptions")]
+        [HttpGet("Libraries/AvailableOptions")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 5b0f46b02e..242cbf1918 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -34,6 +34,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The media info controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class MediaInfoController : BaseJellyfinApiController
     {
@@ -88,7 +89,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id.</param>
         /// <response code="200">Playback info returned.</response>
         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
-        [HttpGet("/Items/{itemId}/PlaybackInfo")]
+        [HttpGet("Items/{itemId}/PlaybackInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
         {
@@ -116,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
         /// <response code="200">Playback info returned.</response>
         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
-        [HttpPost("/Items/{itemId}/PlaybackInfo")]
+        [HttpPost("Items/{itemId}/PlaybackInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
             [FromRoute] Guid itemId,
@@ -237,7 +238,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
         /// <response code="200">Media source opened.</response>
         /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
-        [HttpPost("/LiveStreams/Open")]
+        [HttpPost("LiveStreams/Open")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
             [FromQuery] string? openToken,
@@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="liveStreamId">The livestream id.</param>
         /// <response code="204">Livestream closed.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
-        [HttpPost("/LiveStreams/Close")]
+        [HttpPost("LiveStreams/Close")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CloseLiveStream([FromQuery] string? liveStreamId)
         {
@@ -293,7 +294,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Test buffer returned.</response>
         /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
         /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
-        [HttpGet("/Playback/BitrateTest")]
+        [HttpGet("Playback/BitrateTest")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [Produces(MediaTypeNames.Application.Octet)]
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index a6c552790e..06c4213fb0 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Package Controller.
     /// </summary>
-    [Route("Packages")]
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PackageController : BaseJellyfinApiController
     {
@@ -41,7 +41,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
         /// <response code="200">Package retrieved.</response>
         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
-        [HttpGet("/{name}")]
+        [HttpGet("Packages/{name}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PackageInfo>> GetPackageInfo(
             [FromRoute] [Required] string? name,
@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Available packages returned.</response>
         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
-        [HttpGet]
+        [HttpGet("Packages")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<IEnumerable<PackageInfo>> GetPackages()
         {
@@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Package found.</response>
         /// <response code="404">Package not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
-        [HttpPost("/Installed/{name}")]
+        [HttpPost("Packages/Installed/{name}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Authorize(Policy = Policies.RequiresElevation)]
@@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="packageId">Installation Id.</param>
         /// <response code="204">Installation cancelled.</response>
         /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
-        [HttpDelete("/Installing/{packageId}")]
+        [HttpDelete("Packages/Installing/{packageId}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CancelPackageInstallation(
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Package repositories returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
-        [HttpGet("/Repositories")]
+        [HttpGet("Repositories")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="repositoryInfos">The list of package repositories.</param>
         /// <response code="204">Package repositories saved.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpOptions("/Repositories")]
+        [HttpOptions("Repositories")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 3ebd003f1a..0422bfe727 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Playstate controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PlaystateController : BaseJellyfinApiController
     {
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="datePlayed">Optional. The date the item was played.</param>
         /// <response code="200">Item marked as played.</response>
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpPost("/Users/{userId}/PlayedItems/{itemId}")]
+        [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> MarkPlayedItem(
             [FromRoute] Guid userId,
@@ -93,7 +94,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Item marked as unplayed.</response>
         /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpDelete("/Users/{userId}/PlayedItem/{itemId}")]
+        [HttpDelete("Users/{userId}/PlayedItem/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -115,7 +116,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playbackStartInfo">The playback start info.</param>
         /// <response code="204">Playback start recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Playing")]
+        [HttpPost("Sessions/Playing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
         {
@@ -131,7 +132,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playbackProgressInfo">The playback progress info.</param>
         /// <response code="204">Playback progress recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Playing/Progress")]
+        [HttpPost("Sessions/Playing/Progress")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
         {
@@ -147,7 +148,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playSessionId">Playback session id.</param>
         /// <response code="204">Playback session pinged.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Playing/Ping")]
+        [HttpPost("Sessions/Playing/Ping")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
         {
@@ -161,7 +162,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playbackStopInfo">The playback stop info.</param>
         /// <response code="204">Playback stop recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Playing/Stopped")]
+        [HttpPost("Sessions/Playing/Stopped")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
         {
@@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="canSeek">Indicates if the client can seek.</param>
         /// <response code="204">Play start recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Users/{userId}/PlayingItems/{itemId}")]
+        [HttpPost("Users/{userId}/PlayingItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
         public async Task<ActionResult> OnPlaybackStart(
@@ -240,7 +241,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isMuted">Indicates if the player is muted.</param>
         /// <response code="204">Play progress recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")]
+        [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
         public async Task<ActionResult> OnPlaybackProgress(
@@ -292,7 +293,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playSessionId">The play session id.</param>
         /// <response code="204">Playback stop recorded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Users/{userId}/PlayingItems/{itemId}")]
+        [HttpDelete("Users/{userId}/PlayingItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
         public async Task<ActionResult> OnPlaybackStopped(
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 770d74838d..fe10f0f1bf 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Not Implemented.</returns>
         /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
         [Obsolete("Paid plugins are not supported")]
-        [HttpGet("/Registrations/{name}")]
+        [HttpGet("Registrations/{name}")]
         [ProducesResponseType(StatusCodes.Status501NotImplemented)]
         public ActionResult GetRegistration([FromRoute] string? name)
         {
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 1b300e0d8a..3e6f577f13 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -23,6 +23,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The session controller.
     /// </summary>
+    [Route("")]
     public class SessionController : BaseJellyfinApiController
     {
         private readonly ISessionManager _sessionManager;
@@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
         /// <response code="200">List of sessions returned.</response>
         /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
-        [HttpGet("/Sessions")]
+        [HttpGet("Sessions")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<SessionInfo>> GetSessions(
@@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemName">The name of the item.</param>
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Viewing")]
+        [HttpPost("Sessions/{sessionId}/Viewing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DisplayContent(
             [FromRoute] string? sessionId,
@@ -154,7 +155,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playRequest">The <see cref="PlayRequest"/>.</param>
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Playing")]
+        [HttpPost("Sessions/{sessionId}/Playing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
             [FromRoute] string? sessionId,
@@ -188,7 +189,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
         /// <response code="204">Playstate command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Playing/{command}")]
+        [HttpPost("Sessions/{sessionId}/Playing/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
             [FromRoute] string? sessionId,
@@ -210,7 +211,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="command">The command to send.</param>
         /// <response code="204">System command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/System/{command}")]
+        [HttpPost("Sessions/{sessionId}/System/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
             [FromRoute] string? sessionId,
@@ -241,7 +242,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="command">The command to send.</param>
         /// <response code="204">General command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Command/{command}")]
+        [HttpPost("Sessions/{sessionId}/Command/{command}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
             [FromRoute] string? sessionId,
@@ -267,7 +268,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="command">The <see cref="GeneralCommand"/>.</param>
         /// <response code="204">Full general command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Command")]
+        [HttpPost("Sessions/{sessionId}/Command")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendFullGeneralCommand(
             [FromRoute] string? sessionId,
@@ -300,7 +301,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
         /// <response code="204">Message sent.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/Message")]
+        [HttpPost("Sessions/{sessionId}/Message")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendMessageCommand(
             [FromRoute] string? sessionId,
@@ -327,7 +328,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id.</param>
         /// <response code="204">User added to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/{sessionId}/User/{userId}")]
+        [HttpPost("Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddUserToSession(
             [FromRoute] string? sessionId,
@@ -344,7 +345,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id.</param>
         /// <response code="204">User removed from session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Sessions/{sessionId}/User/{userId}")]
+        [HttpDelete("Sessions/{sessionId}/User/{userId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveUserFromSession(
             [FromRoute] string? sessionId,
@@ -365,7 +366,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
         /// <response code="204">Capabilities posted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Capabilities")]
+        [HttpPost("Sessions/Capabilities")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostCapabilities(
             [FromQuery] string? id,
@@ -398,7 +399,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
         /// <response code="204">Capabilities updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Capabilities/Full")]
+        [HttpPost("Sessions/Capabilities/Full")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostFullCapabilities(
             [FromQuery] string? id,
@@ -421,7 +422,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">The item id.</param>
         /// <response code="204">Session reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Viewing")]
+        [HttpPost("Sessions/Viewing")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportViewing(
             [FromQuery] string? sessionId,
@@ -438,7 +439,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="204">Session end reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Sessions/Logout")]
+        [HttpPost("Sessions/Logout")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportSessionEnded()
         {
@@ -453,7 +454,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Auth providers retrieved.</response>
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
-        [HttpGet("/Auth/Providers")]
+        [HttpGet("Auth/Providers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
         {
@@ -465,7 +466,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Password reset providers retrieved.</response>
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
-        [HttpGet("/Auto/PasswordResetProviders")]
+        [HttpGet("Auto/PasswordResetProviders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
         {
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index f8c19d15c4..22144ab1af 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Subtitle controller.
     /// </summary>
+    [Route("")]
     public class SubtitleController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
@@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Subtitle deleted.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("/Videos/{itemId}/Subtitles/{index}")]
+        [HttpDelete("Videos/{itemId}/Subtitles/{index}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
         /// <response code="200">Subtitles retrieved.</response>
         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
-        [HttpGet("/Items/{itemId}/RemoteSearch/Subtitles/{language}")]
+        [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
@@ -127,7 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="subtitleId">The subtitle id.</param>
         /// <response code="204">Subtitle downloaded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
+        [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
@@ -157,7 +158,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
-        [HttpGet("/Providers/Subtitles/Subtitles/{id}")]
+        [HttpGet("Providers/Subtitles/Subtitles/{id}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
@@ -181,8 +182,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
-        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
-        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
+        [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+        [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetSubtitle(
             [FromRoute, Required] Guid itemId,
@@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="segmentLength">The subtitle segment length.</param>
         /// <response code="200">Subtitle playlist retrieved.</response>
         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
-        [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
+        [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 55759f316f..42db6b6a11 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -16,6 +16,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The suggestions controller.
     /// </summary>
+    [Route("")]
     public class SuggestionsController : BaseJellyfinApiController
     {
         private readonly IDtoService _dtoService;
@@ -49,7 +50,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
         /// <response code="200">Suggestions returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
-        [HttpGet("/Users/{userId}/Suggestions")]
+        [HttpGet("Users/{userId}/Suggestions")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
             [FromRoute] Guid userId,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index fbab7948ff..5157b08ae5 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
         /// <param name="enableImages">Optional, include image information in output.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
-        [HttpGet("/Trailers")]
+        [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
             [FromQuery] Guid? userId,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 5a9bec2b05..75df16aa7b 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The universal audio controller.
     /// </summary>
+    [Route("")]
     public class UniversalAudioController : BaseJellyfinApiController
     {
         private readonly IAuthorizationContext _authorizationContext;
@@ -68,10 +69,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Audio stream returned.</response>
         /// <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=universal}.{container?}", Name = "GetUniversalAudioStream_2")]
-        [HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
-        [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")]
+        [HttpGet("Audio/{itemId}/universal")]
+        [HttpGet("Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")]
+        [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
+        [HttpHead("Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status302Found)]
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 482baf6416..2ce5c7e569 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -450,7 +450,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="request">The create user by name request body.</param>
         /// <response code="200">User created.</response>
         /// <returns>An <see cref="UserDto"/> of the new user.</returns>
-        [HttpPost("/Users/New")]
+        [HttpPost("New")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index cedda3b9d2..f55ff6f3d5 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// User library controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class UserLibraryController : BaseJellyfinApiController
     {
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Item returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the d item.</returns>
-        [HttpGet("/Users/{userId}/Items/{itemId}")]
+        [HttpGet("Users/{userId}/Items/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User id.</param>
         /// <response code="200">Root folder returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
-        [HttpGet("/Users/{userId}/Items/Root")]
+        [HttpGet("Users/{userId}/Items/Root")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<BaseItemDto> GetRootFolder([FromRoute] Guid userId)
         {
@@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Intros returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
-        [HttpGet("/Users/{userId}/Items/{itemId}/Intros")]
+        [HttpGet("Users/{userId}/Items/{itemId}/Intros")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -135,7 +136,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Item marked as favorite.</response>
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpPost("/Users/{userId}/FavoriteItems/{itemId}")]
+        [HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -149,7 +150,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Item unmarked as favorite.</response>
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpDelete("/Users/{userId}/FavoriteItems/{itemId}")]
+        [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -163,7 +164,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Personal rating removed.</response>
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpDelete("/Users/{userId}/Items/{itemId}/Rating")]
+        [HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -178,7 +179,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
         /// <response code="200">Item rating updated.</response>
         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
-        [HttpPost("/Users/{userId}/Items/{itemId}/Rating")]
+        [HttpPost("Users/{userId}/Items/{itemId}/Rating")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool? likes)
         {
@@ -192,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
         /// <returns>The items local trailers.</returns>
-        [HttpGet("/Users/{userId}/Items/{itemId}/LocalTrailers")]
+        [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -227,7 +228,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="itemId">Item id.</param>
         /// <response code="200">Special features returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
-        [HttpGet("/Users/{userId}/Items/{itemId}/SpecialFeatures")]
+        [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId)
         {
@@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="groupItems">Whether or not to group items into a parent container.</param>
         /// <response code="200">Latest media returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
-        [HttpGet("/Users/{userId}/Items/Latest")]
+        [HttpGet("Users/{userId}/Items/Latest")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
             [FromRoute] Guid userId,
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index f4bd451efe..6df7cc779f 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -21,6 +21,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// User views controller.
     /// </summary>
+    [Route("")]
     public class UserViewsController : BaseJellyfinApiController
     {
         private readonly IUserManager _userManager;
@@ -60,7 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="includeHidden">Whether or not to include hidden content.</param>
         /// <response code="200">User views returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
-        [HttpGet("/Users/{userId}/Views")]
+        [HttpGet("Users/{userId}/Views")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
             [FromRoute] Guid userId,
@@ -122,7 +123,7 @@ namespace Jellyfin.Api.Controllers
         /// An <see cref="OkResult"/> containing the user view grouping options
         /// or a <see cref="NotFoundResult"/> if user not found.
         /// </returns>
-        [HttpGet("/Users/{userId}/GroupingOptions")]
+        [HttpGet("Users/{userId}/GroupingOptions")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute] Guid userId)
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 8520dd1638..76188f46d6 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The video hls controller.
     /// </summary>
+    [Route("")]
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class VideoHlsController : BaseJellyfinApiController
     {
@@ -158,7 +159,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
         /// <response code="200">Hls live stream retrieved.</response>
         /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
-        [HttpGet("/Videos/{itemId}/live.m3u8")]
+        [HttpGet("Videos/{itemId}/live.m3u8")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult> GetLiveHlsStream(
             [FromRoute] Guid itemId,
@@ -271,7 +272,7 @@ namespace Jellyfin.Api.Controllers
             };
 
             var cancellationTokenSource = new CancellationTokenSource();
-            var state = await StreamingHelpers.GetStreamingState(
+            using var state = await StreamingHelpers.GetStreamingState(
                     streamingRequest,
                     Request,
                     _authContext,

From 25daa7db42906f715329d1f15938e51b0a397c54 Mon Sep 17 00:00:00 2001
From: Erwin de Haan <EraYaN@users.noreply.github.com>
Date: Tue, 4 Aug 2020 22:35:23 +0200
Subject: [PATCH 433/463] Merge the args and commands item for the artifact
 collection

---
 .ci/azure-pipelines-package.yml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index ab752d0d93..003d5baf04 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -133,8 +133,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh
-      args: /srv/repository/incoming/azure $(Build.BuildNumber) unstable
+      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
@@ -142,9 +141,8 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: sudo -n /srv/repository/collect-server.azure.sh
-      args: /srv/repository/incoming/azure $(Build.BuildNumber)
-
+      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
+      
 - job: PublishNuget
   displayName: 'Publish NuGet packages'
   dependsOn:

From 393666efcfa85e858857334b5cb5a3ec70586f20 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Tue, 4 Aug 2020 20:30:45 -0600
Subject: [PATCH 434/463] fix merge conflicts

---
 Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +-
 Jellyfin.Api/Controllers/SubtitleController.cs   | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index ec65cb95a2..f6f08e873c 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1828,7 +1828,7 @@ namespace Jellyfin.Api.Controllers
                 }
 
                 audioTranscodeParams.Add("-vn");
-                return string.Join(" ", audioTranscodeParams.ToArray());
+                return string.Join(' ', audioTranscodeParams);
             }
 
             if (EncodingHelper.IsCopyCodec(audioCodec))
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 22144ab1af..d5633fba52 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -262,8 +262,6 @@ namespace Jellyfin.Api.Controllers
 
             var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
 
-            var builder = new StringBuilder();
-
             var runtime = mediaSource.RunTimeTicks ?? -1;
 
             if (runtime <= 0)
@@ -277,6 +275,7 @@ namespace Jellyfin.Api.Controllers
                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
             }
 
+            var builder = new StringBuilder();
             builder.AppendLine("#EXTM3U")
                 .Append("#EXT-X-TARGETDURATION:")
                 .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))

From b23446721c0842f0b2a6d0a84b40168ec7a33fc2 Mon Sep 17 00:00:00 2001
From: sharkykh <sharkykh@gmail.com>
Date: Wed, 5 Aug 2020 18:18:54 +0000
Subject: [PATCH 435/463] Translated using Weblate (Hebrew) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/

---
 Emby.Server.Implementations/Localization/Core/he.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 682f5325b5..5f997842c1 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -45,7 +45,7 @@
     "NameSeasonNumber": "עונה {0}",
     "NameSeasonUnknown": "עונה לא ידועה",
     "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
-    "NotificationOptionApplicationUpdateAvailable": "Application update available",
+    "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
@@ -84,8 +84,8 @@
     "UserDeletedWithName": "המשתמש {0} הוסר",
     "UserDownloadingItemWithValues": "{0} מוריד את {1}",
     "UserLockedOutWithName": "User {0} has been locked out",
-    "UserOfflineFromDevice": "{0} has disconnected from {1}",
-    "UserOnlineFromDevice": "{0} is online from {1}",
+    "UserOfflineFromDevice": "{0} התנתק מ-{1}",
+    "UserOnlineFromDevice": "{0} מחובר מ-{1}",
     "UserPasswordChangedWithName": "Password has been changed for user {0}",
     "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
     "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",

From 8bb99096607f2c2791f71d6e83b98eba6ee55d02 Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 5 Aug 2020 21:19:38 +0200
Subject: [PATCH 436/463] Fix photo viewer

---
 Jellyfin.Api/Controllers/LibraryController.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index a4637752d1..4731a5c8b4 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -666,8 +666,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             // TODO determine non-ASCII validity.
-            using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
-            return File(fileStream, MimeTypes.GetMimeType(path), filename);
+            return PhysicalFile(path, MimeTypes.GetMimeType(path));
         }
 
         /// <summary>

From 1eee0fc922630d95d50d305cfe8a356c73b43705 Mon Sep 17 00:00:00 2001
From: sharkykh <sharkykh@gmail.com>
Date: Wed, 5 Aug 2020 18:21:18 +0000
Subject: [PATCH 437/463] Translated using Weblate (Hebrew) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/

---
 .../Localization/Core/he.json                 | 50 +++++++++----------
 1 file changed, 25 insertions(+), 25 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 5f997842c1..dc3a981540 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -18,13 +18,13 @@
     "HeaderAlbumArtists": "אמני האלבום",
     "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderContinueWatching": "המשך לצפות",
-    "HeaderFavoriteAlbums": "אלבומים שאהבתי",
+    "HeaderFavoriteAlbums": "אלבומים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",
     "HeaderFavoriteEpisodes": "פרקים מועדפים",
-    "HeaderFavoriteShows": "סדרות מועדפות",
+    "HeaderFavoriteShows": "תוכניות מועדפות",
     "HeaderFavoriteSongs": "שירים מועדפים",
     "HeaderLiveTV": "שידורים חיים",
-    "HeaderNextUp": "הבא",
+    "HeaderNextUp": "הבא בתור",
     "HeaderRecordingGroups": "קבוצות הקלטה",
     "HomeVideos": "סרטונים בייתים",
     "Inherit": "הורש",
@@ -46,36 +46,36 @@
     "NameSeasonUnknown": "עונה לא ידועה",
     "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
     "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
-    "NotificationOptionApplicationUpdateInstalled": "Application update installed",
-    "NotificationOptionAudioPlayback": "Audio playback started",
-    "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
-    "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+    "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
+    "NotificationOptionAudioPlayback": "ניגון שמע החל",
+    "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
+    "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
     "NotificationOptionInstallationFailed": "התקנה נכשלה",
-    "NotificationOptionNewLibraryContent": "New content added",
-    "NotificationOptionPluginError": "Plugin failure",
+    "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+    "NotificationOptionPluginError": "כשלון בתוסף",
     "NotificationOptionPluginInstalled": "התוסף הותקן",
     "NotificationOptionPluginUninstalled": "התוסף הוסר",
     "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
     "NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
-    "NotificationOptionTaskFailed": "Scheduled task failure",
-    "NotificationOptionUserLockedOut": "User locked out",
-    "NotificationOptionVideoPlayback": "Video playback started",
-    "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
+    "NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
+    "NotificationOptionUserLockedOut": "משתמש ננעל",
+    "NotificationOptionVideoPlayback": "ניגון וידאו החל",
+    "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
     "Photos": "תמונות",
     "Playlists": "רשימות הפעלה",
     "Plugin": "Plugin",
-    "PluginInstalledWithName": "{0} was installed",
-    "PluginUninstalledWithName": "{0} was uninstalled",
-    "PluginUpdatedWithName": "{0} was updated",
+    "PluginInstalledWithName": "{0} הותקן",
+    "PluginUninstalledWithName": "{0} הוסר",
+    "PluginUpdatedWithName": "{0} עודכן",
     "ProviderValue": "Provider: {0}",
-    "ScheduledTaskFailedWithName": "{0} failed",
-    "ScheduledTaskStartedWithName": "{0} started",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ScheduledTaskFailedWithName": "{0} נכשל",
+    "ScheduledTaskStartedWithName": "{0} החל",
+    "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
     "Shows": "סדרות",
     "Songs": "שירים",
     "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
     "Sync": "סנכרן",
     "System": "System",
     "TvShows": "סדרות טלוויזיה",
@@ -83,14 +83,14 @@
     "UserCreatedWithName": "המשתמש {0} נוצר",
     "UserDeletedWithName": "המשתמש {0} הוסר",
     "UserDownloadingItemWithValues": "{0} מוריד את {1}",
-    "UserLockedOutWithName": "User {0} has been locked out",
+    "UserLockedOutWithName": "המשתמש {0} ננעל",
     "UserOfflineFromDevice": "{0} התנתק מ-{1}",
     "UserOnlineFromDevice": "{0} מחובר מ-{1}",
-    "UserPasswordChangedWithName": "Password has been changed for user {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+    "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
+    "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
     "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
     "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
     "ValueSpecialEpisodeName": "מיוחד- {0}",
     "VersionNumber": "Version {0}",
     "TaskRefreshLibrary": "סרוק ספריית מדיה",
@@ -109,7 +109,7 @@
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TasksChannelsCategory": "ערוצי אינטרנט",
     "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
-    "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
+    "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
     "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
     "TaskRefreshChannels": "רענן ערוץ",
     "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",

From 4ea412f2ab38211d245c9b383021a9abae12632d Mon Sep 17 00:00:00 2001
From: David <daullmer@gmail.com>
Date: Wed, 5 Aug 2020 21:57:01 +0200
Subject: [PATCH 438/463] Fix remote images

---
 .../Controllers/RemoteImageController.cs      | 20 ++++++++++---------
 .../Json/Converters/JsonDoubleConverter.cs    |  2 +-
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 1b26163cfc..50a161ef6e 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -25,8 +25,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Remote Images Controller.
     /// </summary>
-    [Route("Images")]
-    [Authorize(Policy = Policies.DefaultAuthorization)]
+    [Route("")]
     public class RemoteImageController : BaseJellyfinApiController
     {
         private readonly IProviderManager _providerManager;
@@ -65,7 +64,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Remote Images returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>Remote Image Result.</returns>
-        [HttpGet("{itemId}/RemoteImages")]
+        [HttpGet("Items/{itemId}/RemoteImages")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
@@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ImageType? type,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string providerName,
+            [FromQuery] string? providerName,
             [FromQuery] bool includeAllLanguages = false)
         {
             var item = _libraryManager.GetItemById(itemId);
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
 
             var images = await _providerManager.GetAvailableRemoteImages(
                     item,
-                    new RemoteImageQuery(providerName)
+                    new RemoteImageQuery(providerName ?? string.Empty)
                     {
                         IncludeAllLanguages = includeAllLanguages,
                         IncludeDisabledProviders = true,
@@ -128,7 +128,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Returned remote image providers.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>List of remote image providers.</returns>
-        [HttpGet("{itemId}/RemoteImages/Providers")]
+        [HttpGet("Items/{itemId}/RemoteImages/Providers")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId)
@@ -149,11 +150,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Remote image returned.</response>
         /// <response code="404">Remote image not found.</response>
         /// <returns>Image Stream.</returns>
-        [HttpGet("Remote")]
+        [HttpGet("Images/Remote")]
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
+        public async Task<ActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
         {
             var urlHash = imageUrl.GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
@@ -202,7 +203,8 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Remote image downloaded.</response>
         /// <response code="404">Remote image not found.</response>
         /// <returns>Download status.</returns>
-        [HttpPost("{itemId}/RemoteImages/Download")]
+        [HttpPost("Items/{itemId}/RemoteImages/Download")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> DownloadRemoteImage(
diff --git a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
index e5e9f28dae..56c0ecbe9c 100644
--- a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Common.Json.Converters
         /// <param name="options">Options.</param>
         public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
         {
-            writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo));
+            writer.WriteNumberValue(value);
         }
     }
 }

From 7a1f140e5b26e4429fff2acde8dd7812132b910a Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface" <joshua@boniface.me>
Date: Thu, 6 Aug 2020 02:04:41 -0400
Subject: [PATCH 439/463] Bump to .NET Core SDK 3.1.302

---
 deployment/Dockerfile.debian.amd64  | 2 +-
 deployment/Dockerfile.debian.arm64  | 2 +-
 deployment/Dockerfile.debian.armhf  | 2 +-
 deployment/Dockerfile.linux.amd64   | 2 +-
 deployment/Dockerfile.macos         | 2 +-
 deployment/Dockerfile.portable      | 2 +-
 deployment/Dockerfile.ubuntu.amd64  | 2 +-
 deployment/Dockerfile.ubuntu.arm64  | 2 +-
 deployment/Dockerfile.ubuntu.armhf  | 2 +-
 deployment/Dockerfile.windows.amd64 | 2 +-
 10 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index b5a0380489..f9c6a16748 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index cfe562df33..5c08444df0 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index ea8c8c8e62..b54fc3f916 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index d8bec92145..3a2f67615c 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index ba5da40190..d1c0784ff8 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 2893e140df..7270188fd1 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index e61be4efcc..b7d3b4bde1 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index f91b91cd46..dc90f9fbdf 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 85414614c0..db98610c9d 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index 0397a023e2..95fd10cf40 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

From fffa94fc33b923863e7cfe0d57d85ae86206975e Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 6 Aug 2020 08:17:45 -0600
Subject: [PATCH 440/463] Apply fixes from review

---
 .../FirstTimeSetupOrDefaultHandler.cs         | 56 +++++++++++++++++++
 .../FirstTimeSetupOrDefaultRequirement.cs     | 11 ++++
 ...eParentalControlOrFirstTimeSetupHandler.cs | 51 +++++++++++++++++
 ...entalControlOrFirstTimeSetupRequirement.cs | 11 ++++
 .../IgnoreParentalControlHandler.cs}          | 10 ++--
 .../IgnoreParentalControlRequirement.cs}      |  4 +-
 .../LocalAccessOrRequiresElevationHandler.cs  | 46 +++++++++++++++
 ...calAccessOrRequiresElevationRequirement.cs | 11 ++++
 Jellyfin.Api/Constants/Policies.cs            | 19 ++++++-
 Jellyfin.Api/Controllers/ApiKeyController.cs  |  2 +-
 .../Controllers/CollectionController.cs       |  5 +-
 .../Controllers/ConfigurationController.cs    |  6 +-
 Jellyfin.Api/Controllers/DevicesController.cs | 14 ++---
 .../DisplayPreferencesController.cs           |  7 +--
 .../Controllers/EnvironmentController.cs      |  8 +--
 .../Controllers/ImageByNameController.cs      | 11 ++--
 Jellyfin.Api/Controllers/ImageController.cs   |  2 +
 .../Controllers/InstantMixController.cs       |  3 +-
 .../Controllers/ItemLookupController.cs       | 21 ++++---
 .../Controllers/ItemUpdateController.cs       |  6 +-
 Jellyfin.Api/Controllers/LibraryController.cs |  9 +--
 .../Controllers/LibraryStructureController.cs |  4 +-
 .../Controllers/LocalizationController.cs     |  2 +-
 .../Controllers/MediaInfoController.cs        |  5 +-
 .../Controllers/NotificationsController.cs    | 14 +++--
 Jellyfin.Api/Controllers/PackageController.cs |  1 -
 Jellyfin.Api/Controllers/PersonsController.cs |  3 +
 .../Controllers/PlaylistsController.cs        |  4 +-
 Jellyfin.Api/Controllers/PluginsController.cs |  4 +-
 .../Controllers/RemoteImageController.cs      |  6 +-
 .../Controllers/ScheduledTasksController.cs   | 10 ++--
 Jellyfin.Api/Controllers/SessionController.cs | 49 ++++++++++------
 .../Controllers/SubtitleController.cs         |  6 +-
 .../Controllers/SyncPlayController.cs         |  2 +-
 Jellyfin.Api/Controllers/SystemController.cs  |  7 +--
 .../Controllers/TimeSyncController.cs         |  4 +-
 Jellyfin.Api/Controllers/TvShowsController.cs | 13 +++--
 Jellyfin.Api/Controllers/UserController.cs    | 11 ++--
 .../Controllers/VideoAttachmentsController.cs | 10 ++--
 Jellyfin.Api/Controllers/VideosController.cs  |  4 +-
 Jellyfin.Api/Controllers/YearsController.cs   |  3 +
 .../StartupDtos/StartupConfigurationDto.cs    |  2 +-
 .../ApiServiceCollectionExtensions.cs         | 35 ++++++++++--
 .../IgnoreScheduleHandlerTests.cs             |  8 +--
 44 files changed, 386 insertions(+), 134 deletions(-)
 create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
 create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
 create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
 create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
 rename Jellyfin.Api/Auth/{IgnoreSchedulePolicy/IgnoreScheduleHandler.cs => IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs} (77%)
 rename Jellyfin.Api/Auth/{IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs => IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs} (51%)
 create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
 create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs

diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
new file mode 100644
index 0000000000..67fb2b79a1
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -0,0 +1,56 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
+{
+    /// <summary>
+    /// Authorization handler for requiring first time setup or elevated privileges.
+    /// </summary>
+    public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
+    {
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
+        /// </summary>
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public FirstTimeSetupOrDefaultHandler(
+            IConfigurationManager configurationManager,
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+            _configurationManager = configurationManager;
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement)
+        {
+            if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            {
+                context.Succeed(firstTimeSetupOrElevatedRequirement);
+                return Task.CompletedTask;
+            }
+
+            var validated = ValidateClaims(context.User);
+            if (validated)
+            {
+                context.Succeed(firstTimeSetupOrElevatedRequirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
new file mode 100644
index 0000000000..23d7ee01f3
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
+{
+    /// <summary>
+    /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
+    /// </summary>
+    public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
new file mode 100644
index 0000000000..6c9258b3dc
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
@@ -0,0 +1,51 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
+{
+    /// <summary>
+    /// Escape schedule controls handler.
+    /// </summary>
+    public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
+    {
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgnoreParentalControlOrFirstTimeSetupHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        public IgnoreParentalControlOrFirstTimeSetupHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor,
+            IConfigurationManager configurationManager)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+            _configurationManager = configurationManager;
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, ignoreSchedule: true);
+            if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            {
+                context.Succeed(requirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
new file mode 100644
index 0000000000..36ded06250
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
+{
+    /// <summary>
+    /// Escape schedule controls requirement.
+    /// </summary>
+    public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
similarity index 77%
rename from Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
rename to Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
index 9afa0b28f1..5213bc4cb7 100644
--- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
@@ -4,20 +4,20 @@ using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 
-namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
 {
     /// <summary>
     /// Escape schedule controls handler.
     /// </summary>
-    public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
+    public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
+        /// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
         /// </summary>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
-        public IgnoreScheduleHandler(
+        public IgnoreParentalControlHandler(
             IUserManager userManager,
             INetworkManager networkManager,
             IHttpContextAccessor httpContextAccessor)
@@ -26,7 +26,7 @@ namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
         }
 
         /// <inheritdoc />
-        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
         {
             var validated = ValidateClaims(context.User, ignoreSchedule: true);
             if (!validated)
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
similarity index 51%
rename from Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
rename to Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
index d5bb61ce6c..cdad74270e 100644
--- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
+++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
@@ -1,11 +1,11 @@
 using Microsoft.AspNetCore.Authorization;
 
-namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
 {
     /// <summary>
     /// Escape schedule controls requirement.
     /// </summary>
-    public class IgnoreScheduleRequirement : IAuthorizationRequirement
+    public class IgnoreParentalControlRequirement : IAuthorizationRequirement
     {
     }
 }
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
new file mode 100644
index 0000000000..d9ab8aa687
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -0,0 +1,46 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
+{
+    /// <summary>
+    /// Local access handler.
+    /// </summary>
+    public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
+        /// </summary>
+        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+        public LocalAccessOrRequiresElevationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, localAccessOnly: true);
+
+            if (validated || context.User.IsInRole(UserRoles.Administrator))
+            {
+                context.Succeed(requirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
new file mode 100644
index 0000000000..ad96caa811
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
+{
+    /// <summary>
+    /// The local access authorization requirement.
+    /// </summary>
+    public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 851b56d732..8de637c4e9 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Constants
         /// <summary>
         /// Policy name for requiring first time setup or elevated privileges.
         /// </summary>
-        public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated";
+        public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
 
         /// <summary>
         /// Policy name for requiring elevated privileges.
@@ -28,11 +28,26 @@ namespace Jellyfin.Api.Constants
         /// <summary>
         /// Policy name for escaping schedule controls.
         /// </summary>
-        public const string IgnoreSchedule = "IgnoreSchedule";
+        public const string IgnoreParentalControl = "IgnoreParentalControl";
 
         /// <summary>
         /// Policy name for requiring download permission.
         /// </summary>
         public const string Download = "Download";
+
+        /// <summary>
+        /// Policy name for requiring first time setup or default permissions.
+        /// </summary>
+        public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
+
+        /// <summary>
+        /// Policy name for requiring local access or elevated privileges.
+        /// </summary>
+        public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
+
+        /// <summary>
+        /// Policy name for escaping schedule controls or requiring first time setup.
+        /// </summary>
+        public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup";
     }
 }
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index ccb7f47f08..0e28d4c474 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Keys/{key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RevokeKey([FromRoute] string? key)
+        public ActionResult RevokeKey([FromRoute, Required] string? key)
         {
             _sessionManager.RevokeToken(key);
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index b63fc5ab19..53821a1885 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
@@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
+        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
         {
             _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpDelete("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
+        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
         {
             _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 7d262ed595..019703dae9 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
 using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -9,7 +10,6 @@ using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Configuration")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
+        public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
         {
             _configurationManager.ReplaceConfiguration(configuration);
             return NoContent();
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("MediaEncoder/Path")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
+        public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath)
         {
             _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 3cf7b33785..23d10e2156 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Security;
@@ -8,7 +9,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
             return _deviceManager.GetDevices(deviceQuery);
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string? id)
+        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
         {
             var deviceInfo = _deviceManager.GetDevice(id);
             if (deviceInfo == null)
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string? id)
+        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
         {
             var deviceInfo = _deviceManager.GetDeviceOptions(id);
             if (deviceInfo == null)
@@ -111,8 +111,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateDeviceOptions(
-            [FromQuery, BindRequired] string? id,
-            [FromBody, BindRequired] DeviceOptions deviceOptions)
+            [FromQuery, Required] string? id,
+            [FromBody, Required] DeviceOptions deviceOptions)
         {
             var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
             if (existingDeviceOptions == null)
@@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult DeleteDevice([FromQuery, BindRequired] string? id)
+        public ActionResult DeleteDevice([FromQuery, Required] string? id)
         {
             var existingDevice = _deviceManager.GetDevice(id);
             if (existingDevice == null)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 62f6097f36..c547d0cde3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -99,9 +98,9 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
         public ActionResult UpdateDisplayPreferences(
             [FromRoute] string? displayPreferencesId,
-            [FromQuery, BindRequired] Guid userId,
-            [FromQuery, BindRequired] string? client,
-            [FromBody, BindRequired] DisplayPreferencesDto displayPreferences)
+            [FromQuery, Required] Guid userId,
+            [FromQuery, Required] string? client,
+            [FromBody, Required] DisplayPreferencesDto displayPreferences)
         {
             HomeSectionType[] defaults =
             {
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 719bb7d86d..64670f7d84 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using Jellyfin.Api.Constants;
@@ -8,7 +9,6 @@ using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("DirectoryContents")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
-            [FromQuery, BindRequired] string path,
+            [FromQuery, Required] string path,
             [FromQuery] bool includeFiles = false,
             [FromQuery] bool includeDirectories = false)
         {
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("ValidatePath")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto)
+        public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
         {
             if (validatePathDto.IsFile.HasValue)
             {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Parent path.</returns>
         [HttpGet("ParentPath")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<string?> GetParentPath([FromQuery, BindRequired] string path)
+        public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
         {
             string? parent = Path.GetDirectoryName(path);
             if (string.IsNullOrEmpty(parent))
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 5244c35b89..5285905365 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using System.Net.Mime;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string? name, [FromRoute] string? type)
+        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
         {
             var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
                 ? "folder"
@@ -110,8 +111,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetRatingImage(
-            [FromRoute] string? theme,
-            [FromRoute] string? name)
+            [FromRoute, Required] string? theme,
+            [FromRoute, Required] string? name)
         {
             return GetImageFile(_applicationPaths.RatingsPath, theme, name);
         }
@@ -143,8 +144,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetMediaInfoImage(
-            [FromRoute] string? theme,
-            [FromRoute] string? name)
+            [FromRoute, Required] string? theme,
+            [FromRoute, Required] string? name)
         {
             return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
         }
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 360164ad4f..410456a25c 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -84,6 +84,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Users/{userId}/Images/{imageType}")]
         [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -259,6 +260,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpGet("Items/{itemId}/Images")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 8ca232ceff..73bd30c4d1 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
@@ -174,7 +175,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
-            [FromRoute] string? name,
+            [FromRoute, Required] string? name,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 0d9dffbfe6..c9ad15babe 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -22,7 +22,6 @@ using MediaBrowser.Model.Providers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
@@ -94,7 +93,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Movie")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MovieInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -111,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Trailer")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<TrailerInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -128,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicVideo")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MusicVideoInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -145,7 +144,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Series")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<SeriesInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -162,7 +161,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/BoxSet")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BoxSetInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -179,7 +178,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicArtist")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<ArtistInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -196,7 +195,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicAlbum")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<AlbumInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -214,7 +213,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpPost("Items/RemoteSearch/Person")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<PersonLookupInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -231,7 +230,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Book")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BookInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> ApplySearchCriteria(
             [FromRoute] Guid itemId,
-            [FromBody, BindRequired] RemoteSearchResult searchResult,
+            [FromBody, Required] RemoteSearchResult searchResult,
             [FromQuery] bool replaceAllImages = true)
         {
             var item = _libraryManager.GetItemById(itemId);
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index a5d9d36a33..4b40c6ada9 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;
@@ -17,7 +18,6 @@ using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -67,7 +67,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Items/{itemId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request)
+        public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Items/{itemId}/ContentType")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType)
+        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 4731a5c8b4..4548e202a0 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -32,7 +33,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Movie = Jellyfin.Data.Entities.Movie;
@@ -597,7 +597,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates)
+        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
         {
             foreach (var item in updates)
             {
@@ -685,6 +685,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
         [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
         [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
@@ -736,11 +737,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Library options info returned.</response>
         /// <returns>Library options info.</returns>
         [HttpGet("Libraries/AvailableOptions")]
-        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
             [FromQuery] string? libraryContentType,
-            [FromQuery] bool isNewLibrary = false)
+            [FromQuery] bool isNewLibrary)
         {
             var result = new LibraryOptionsResultDto();
 
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index ca150f3f24..cdab4f356f 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -17,7 +18,6 @@ using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Paths")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddMediaPath(
-            [FromBody, BindRequired] MediaPathDto mediaPathDto,
+            [FromBody, Required] MediaPathDto mediaPathDto,
             [FromQuery] bool refreshLibrary = false)
         {
             _libraryMonitor.Stop();
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index 1466dd3ec0..ef2e7e8b15 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Localization controller.
     /// </summary>
-    [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+    [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
     public class LocalizationController : BaseJellyfinApiController
     {
         private readonly ILocalizationManager _localization;
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 242cbf1918..517113074c 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Buffers;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using System.Net.Mime;
@@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
         [HttpGet("Items/{itemId}/PlaybackInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
         {
             return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
         }
@@ -281,7 +282,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("LiveStreams/Close")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CloseLiveStream([FromQuery] string? liveStreamId)
+        public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId)
         {
             _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
             return NoContent();
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 1bb39b5f76..47ce48b2da 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,13 +1,16 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.NotificationDtos;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Notifications;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The notification controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class NotificationsController : BaseJellyfinApiController
     {
         private readonly INotificationManager _notificationManager;
@@ -83,19 +87,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Sends a notification to all admins.
         /// </summary>
-        /// <param name="name">The name of the notification.</param>
-        /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
+        /// <param name="name">The name of the notification.</param>
+        /// <param name="description">The description of the notification.</param>
         /// <response code="204">Notification sent.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateAdminNotification(
-            [FromQuery] string? name,
-            [FromQuery] string? description,
             [FromQuery] string? url,
-            [FromQuery] NotificationLevel? level)
+            [FromQuery] NotificationLevel? level,
+            [FromQuery, Required] string name = "",
+            [FromQuery, Required] string description = "")
         {
             var notification = new NotificationRequest
             {
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 06c4213fb0..3d6a879093 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -127,7 +127,6 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Package repositories returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
         [HttpGet("Repositories")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
         {
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 23cc23ce70..b6ccec666a 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Persons controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PersonsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index cf46604948..12c87d7c36 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -14,7 +15,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
-            [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest)
+            [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
         {
             Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
             var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index fe10f0f1bf..b2f34680b0 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -13,7 +14,6 @@ using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("SecurityInfo")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
+        public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
         {
             return NoContent();
         }
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 50a161ef6e..baa3d80ac8 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using System.Net.Mime;
@@ -18,7 +19,6 @@ using MediaBrowser.Model.Providers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public async Task<ActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
+        public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
         {
             var urlHash = imageUrl.GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
@@ -209,7 +209,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> DownloadRemoteImage(
             [FromRoute] Guid itemId,
-            [FromQuery, BindRequired] ImageType type,
+            [FromQuery, Required] ImageType type,
             [FromQuery] string? imageUrl)
         {
             var item = _libraryManager.GetItemById(itemId);
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 3df325e3ba..e672070c06 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -1,12 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Model.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<TaskInfo> GetTask([FromRoute] string? taskId)
+        public ActionResult<TaskInfo> GetTask([FromRoute, Required] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
                 string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult StopTask([FromRoute] string? taskId)
+        public ActionResult StopTask([FromRoute, Required] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -144,8 +144,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateTask(
-            [FromRoute] string? taskId,
-            [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
+            [FromRoute, Required] string? taskId,
+            [FromBody, Required] TaskTriggerInfo[] triggerInfos)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 3e6f577f13..48b57bdb7f 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -122,12 +122,13 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Viewing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DisplayContent(
-            [FromRoute] string? sessionId,
-            [FromQuery] string? itemType,
-            [FromQuery] string? itemId,
-            [FromQuery] string? itemName)
+            [FromRoute, Required] string? sessionId,
+            [FromQuery, Required] string? itemType,
+            [FromQuery, Required] string? itemId,
+            [FromQuery, Required] string? itemName)
         {
             var command = new BrowseRequest
             {
@@ -156,9 +157,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Playing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromQuery] Guid[] itemIds,
             [FromQuery] long? startPositionTicks,
             [FromQuery] PlayCommand playCommand,
@@ -190,9 +192,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Playstate command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Playing/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromBody] PlaystateRequest playstateRequest)
         {
             _sessionManager.SendPlaystateCommand(
@@ -212,10 +215,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">System command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/System/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
-            [FromRoute] string? sessionId,
-            [FromRoute] string? command)
+            [FromRoute, Required] string? sessionId,
+            [FromRoute, Required] string? command)
         {
             var name = command;
             if (Enum.TryParse(name, true, out GeneralCommandType commandType))
@@ -243,10 +247,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">General command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Command/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
-            [FromRoute] string? sessionId,
-            [FromRoute] string? command)
+            [FromRoute, Required] string? sessionId,
+            [FromRoute, Required] string? command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
@@ -269,9 +274,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Full general command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Command")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendFullGeneralCommand(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromBody, Required] GeneralCommand command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -302,11 +308,12 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Message sent.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Message")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendMessageCommand(
-            [FromRoute] string? sessionId,
-            [FromQuery] string? text,
-            [FromQuery] string? header,
+            [FromRoute, Required] string? sessionId,
+            [FromQuery, Required] string? text,
+            [FromQuery, Required] string? header,
             [FromQuery] long? timeoutMs)
         {
             var command = new MessageCommand
@@ -329,9 +336,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">User added to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/User/{userId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddUserToSession(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromRoute] Guid userId)
         {
             _sessionManager.AddAdditionalUser(sessionId, userId);
@@ -346,6 +354,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">User removed from session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("Sessions/{sessionId}/User/{userId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveUserFromSession(
             [FromRoute] string? sessionId,
@@ -367,9 +376,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Capabilities posted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Capabilities")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostCapabilities(
-            [FromQuery] string? id,
+            [FromQuery, Required] string? id,
             [FromQuery] string? playableMediaTypes,
             [FromQuery] string? supportedCommands,
             [FromQuery] bool supportsMediaControl = false,
@@ -400,9 +410,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Capabilities updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Capabilities/Full")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostFullCapabilities(
-            [FromQuery] string? id,
+            [FromQuery, Required] string? id,
             [FromBody, Required] ClientCapabilities capabilities)
         {
             if (string.IsNullOrWhiteSpace(id))
@@ -423,6 +434,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Session reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Viewing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportViewing(
             [FromQuery] string? sessionId,
@@ -440,6 +452,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Session end reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Logout")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportSessionEnded()
         {
@@ -455,6 +468,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Auth providers retrieved.</response>
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
         [HttpGet("Auth/Providers")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
         {
@@ -468,6 +482,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
         [HttpGet("Auto/PasswordResetProviders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
         {
             return _userManager.GetPasswordResetProviders();
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index d5633fba52..988acccc3a 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string? language,
+            [FromRoute, Required] string? language,
             [FromQuery] bool? isPerfectMatch)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string? subtitleId)
+            [FromRoute, Required] string? subtitleId)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
 
@@ -162,7 +162,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
-        public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string? id)
+        public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
         {
             var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
 
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 2b1b95b1b5..e16a10ba4d 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="filterItemId">Optional. Filter by item id.</param>
         /// <response code="200">Groups returned.</response>
-        /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
+        /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
         [HttpGet("List")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 6f9a75e2f5..08f1b421db 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -23,7 +23,6 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The system controller.
     /// </summary>
-    [Route("System")]
     public class SystemController : BaseJellyfinApiController
     {
         private readonly IServerApplicationHost _appHost;
@@ -60,8 +59,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
         [HttpGet("Info")]
-        [Authorize(Policy = Policies.IgnoreSchedule)]
-        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<SystemInfo>> GetSystemInfo()
         {
@@ -99,8 +97,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Server restarted.</response>
         /// <returns>No content. Server restarted.</returns>
         [HttpPost("Restart")]
-        [Authorize(Policy = Policies.LocalAccessOnly)]
-        [Authorize(Policy = Policies.RequiresElevation)]
+        [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RestartApplication()
         {
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index bbabcd6e60..2dc744e7ca 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The time sync controller.
     /// </summary>
-    [Route("GetUtcTime")]
+    [Route("")]
     public class TimeSyncController : BaseJellyfinApiController
     {
         /// <summary>
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Time returned.</response>
         /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
-        [HttpGet]
+        [HttpGet("GetUtcTime")]
         [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
         public ActionResult<UtcTimeResponse> GetUtcTime()
         {
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index d4560dfa25..f463ab8894 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
@@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("NextUp")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
-            [FromQuery] Guid? userId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Upcoming")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
-            [FromQuery] Guid? userId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -193,8 +194,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
-            [FromRoute] string? seriesId,
-            [FromQuery] Guid? userId,
+            [FromRoute, Required] string? seriesId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] int? season,
             [FromQuery] string? seasonId,
@@ -316,8 +317,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
-            [FromRoute] string? seriesId,
-            [FromQuery] Guid? userId,
+            [FromRoute, Required] string? seriesId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 2ce5c7e569..d897f07b7f 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -20,7 +20,6 @@ using MediaBrowser.Model.Users;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -106,7 +105,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">User not found.</response>
         /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
         [HttpGet("{userId}")]
-        [Authorize(Policy = Policies.IgnoreSchedule)]
+        [Authorize(Policy = Policies.IgnoreParentalControl)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
@@ -157,8 +156,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
             [FromRoute, Required] Guid userId,
-            [FromQuery, BindRequired] string? pw,
-            [FromQuery, BindRequired] string? password)
+            [FromQuery, Required] string? pw,
+            [FromQuery] string? password)
         {
             var user = _userManager.GetUserById(userId);
 
@@ -190,7 +189,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
         [HttpPost("AuthenticateByName")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request)
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
         {
             var auth = _authContext.GetAuthorizationInfo(Request);
 
@@ -371,7 +370,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User policy update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
         [HttpPost("{userId}/Policy")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index eef0a93cdf..09a1c93e6a 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -1,12 +1,11 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,7 +15,6 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class VideoAttachmentsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
@@ -49,9 +47,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<FileStreamResult>> GetAttachment(
-            [FromRoute] Guid videoId,
-            [FromRoute] string? mediaSourceId,
-            [FromRoute] int index)
+            [FromRoute, Required] Guid videoId,
+            [FromRoute, Required] string mediaSourceId,
+            [FromRoute, Required] int index)
         {
             try
             {
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index ebe88a9c05..fe065c76f6 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using System.Net.Http;
@@ -35,7 +36,6 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The videos controller.
     /// </summary>
-    [Route("Videos")]
     public class VideosController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
-        public ActionResult MergeVersions([FromQuery] string? itemIds)
+        public ActionResult MergeVersions([FromQuery, Required] string? itemIds)
         {
             var items = RequestHelpers.Split(itemIds, ',', true)
                 .Select(i => _libraryManager.GetItemById(i))
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index d09b016a9a..eb91ac23e9 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Years controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class YearsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index a5f012245a..66e7976996 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos
         /// <summary>
         /// Gets or sets UI language culture.
         /// </summary>
-        public string? UICulture { get; set; }
+        public string UICulture { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets the metadata country code.
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 6e91042dfd..586746430a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -7,8 +7,11 @@ using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.DownloadPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
-using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
@@ -41,9 +44,12 @@ namespace Jellyfin.Server.Extensions
         {
             serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
-            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlOrFirstTimeSetupHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
             return serviceCollection.AddAuthorizationCore(options =>
             {
@@ -61,6 +67,13 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new DownloadRequirement());
                     });
+                options.AddPolicy(
+                    Policies.FirstTimeSetupOrDefault,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement());
+                    });
                 options.AddPolicy(
                     Policies.FirstTimeSetupOrElevated,
                     policy =>
@@ -69,11 +82,18 @@ namespace Jellyfin.Server.Extensions
                         policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
                     });
                 options.AddPolicy(
-                    Policies.IgnoreSchedule,
+                    Policies.IgnoreParentalControl,
                     policy =>
                     {
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
-                        policy.AddRequirements(new IgnoreScheduleRequirement());
+                        policy.AddRequirements(new IgnoreParentalControlRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.IgnoreParentalControlOrFirstTimeSetup,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement());
                     });
                 options.AddPolicy(
                     Policies.LocalAccessOnly,
@@ -82,6 +102,13 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new LocalAccessRequirement());
                     });
+                options.AddPolicy(
+                    Policies.LocalAccessOrRequiresElevation,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement());
+                    });
                 options.AddPolicy(
                     Policies.RequiresElevation,
                     policy =>
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
index b65d45aa08..7150c90bb8 100644
--- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
     {
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
-        private readonly IgnoreScheduleHandler _sut;
+        private readonly IgnoreParentalControlHandler _sut;
         private readonly Mock<IUserManager> _userManagerMock;
         private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
@@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
         {
             var fixture = new Fixture().Customize(new AutoMoqCustomization());
             _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
-            _requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
+            _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() };
             _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
             _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
 
-            _sut = fixture.Create<IgnoreScheduleHandler>();
+            _sut = fixture.Create<IgnoreParentalControlHandler>();
         }
 
         [Theory]

From 57585273e366026cef2d3530261a6e70a481b2b1 Mon Sep 17 00:00:00 2001
From: millallo <millallo@tiscali.it>
Date: Thu, 6 Aug 2020 16:09:32 +0000
Subject: [PATCH 441/463] Translated using Weblate (Italian) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/

---
 Emby.Server.Implementations/Localization/Core/it.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 7f5a56e86c..0e27806dd9 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -84,8 +84,8 @@
     "UserDeletedWithName": "L'utente {0} è stato rimosso",
     "UserDownloadingItemWithValues": "{0} sta scaricando {1}",
     "UserLockedOutWithName": "L'utente {0} è stato bloccato",
-    "UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
-    "UserOnlineFromDevice": "{0} è online da {1}",
+    "UserOfflineFromDevice": "{0} si è disconnesso su {1}",
+    "UserOnlineFromDevice": "{0} è online su {1}",
     "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
     "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
     "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",

From bd0ad4196ad77a0349d4d60737cb4056138188cf Mon Sep 17 00:00:00 2001
From: sumantrabhattacharya <bsumantra98@gmail.com>
Date: Thu, 6 Aug 2020 19:39:34 +0000
Subject: [PATCH 442/463] Translated using Weblate (Bengali) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/

---
 Emby.Server.Implementations/Localization/Core/bn.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 1f309f3ff4..ca14d44715 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -7,7 +7,7 @@
     "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
     "Books": "বই",
     "AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
-    "Artists": "শিল্পী",
+    "Artists": "শিল্পীরা",
     "Application": "অ্যাপ্লিকেশন",
     "Albums": "অ্যালবামগুলো",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
@@ -19,7 +19,7 @@
     "Genres": "ঘরানা",
     "Folders": "ফোল্ডারগুলো",
     "Favorites": "ফেভারিটগুলো",
-    "FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
+    "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
     "AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
     "VersionNumber": "সংস্করণ {0}",
     "ValueSpecialEpisodeName": "বিশেষ - {0}",

From f8710876453a468524d8a5dd951771cd4c6302de Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Thu, 6 Aug 2020 23:37:11 +0200
Subject: [PATCH 443/463] Make external ids nullable in TMDb

---
 .../Plugins/Tmdb/Models/General/ExternalIds.cs              | 4 ++--
 .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs                  | 2 +-
 .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs                   | 2 +-
 .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs                   | 6 +++---
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
index 310c871ec6..aac4420e8b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
@@ -10,8 +10,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
 
         public string Freebase_Mid { get; set; }
 
-        public int Tvdb_Id { get; set; }
+        public int? Tvdb_Id { get; set; }
 
-        public int Tvrage_Id { get; set; }
+        public int? Tvrage_Id { get; set; }
     }
 }
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 0c55b91e0a..a07ceb40ec 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -111,7 +111,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
                 if (response.External_Ids.Tvdb_Id > 0)
                 {
-                    item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+                    item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
                 }
 
                 item.PremiereDate = response.Air_Date;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 11f21333cd..822dc4317f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
                     if (seasonInfo.External_Ids.Tvdb_Id > 0)
                     {
-                        result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+                        result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
                     }
 
                     var credits = seasonInfo.Credits;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index f142fd29c7..3480a15f64 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
                 if (obj.External_Ids.Tvdb_Id > 0)
                 {
-                    remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.ToString(_usCulture));
+                    remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
                 }
 
                 return new[] { remoteResult };
@@ -268,12 +268,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
                 if (ids.Tvrage_Id > 0)
                 {
-                    series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.ToString(_usCulture));
+                    series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture));
                 }
 
                 if (ids.Tvdb_Id > 0)
                 {
-                    series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.ToString(_usCulture));
+                    series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture));
                 }
             }
 

From 23dfadd4300e251b737d4126d5e361921263113c Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Thu, 6 Aug 2020 23:43:19 +0200
Subject: [PATCH 444/463] Throw HttpException when tvdb sends us crap data

---
 MediaBrowser.Providers/Manager/ProviderManager.cs | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 9170c70025..4e8ff24d35 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -4,6 +4,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Net;
+using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -22,6 +24,7 @@ using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;
 using Microsoft.Extensions.Logging;
 using Priority_Queue;
@@ -169,6 +172,15 @@ namespace MediaBrowser.Providers.Manager
                 }
             }
 
+            // thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
+            if (response.ContentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
+            {
+                throw new HttpException("Invalid image received.")
+                {
+                    StatusCode = HttpStatusCode.NotFound
+                };
+            }
+
             await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
         }
 

From 268139c435eab7c880d8d20069dadae043928013 Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Thu, 6 Aug 2020 23:47:30 +0200
Subject: [PATCH 445/463] Remove rate limit from TMDb provider

---
 .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs      | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 74870c9992..27ab6756f1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -406,26 +406,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             return mainResult;
         }
 
-        private static long _lastRequestTicks;
-        // The limit is 40 requests per 10 seconds
-        private const int RequestIntervalMs = 300;
-
         /// <summary>
         /// Gets the movie db response.
         /// </summary>
         internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
         {
-            var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks);
-            var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs);
-
-            if (delayMs > 0)
-            {
-                _logger.LogDebug("Throttling Tmdb by {0} ms", delayMs);
-                await Task.Delay(Convert.ToInt32(delayMs)).ConfigureAwait(false);
-            }
-
-            _lastRequestTicks = DateTime.UtcNow.Ticks;
-
             options.BufferContent = true;
             options.UserAgent = _appHost.ApplicationUserAgent;
 

From 05f9473544dd40c6ffdc8512d675256468c09cb3 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Thu, 6 Aug 2020 17:59:48 -0600
Subject: [PATCH 446/463] Apply fixes from review

---
 ...TimeOrIgnoreParentalControlSetupHandler.cs} | 18 ++++++++++++------
 ...eOrIgnoreParentalControlSetupRequirement.cs | 11 +++++++++++
 .../FirstTimeSetupOrDefaultHandler.cs          |  8 ++++----
 .../FirstTimeSetupOrDefaultRequirement.cs      |  2 +-
 ...rentalControlOrFirstTimeSetupRequirement.cs | 11 -----------
 .../LocalAccessOrRequiresElevationHandler.cs   |  3 +--
 ...ocalAccessOrRequiresElevationRequirement.cs |  2 +-
 Jellyfin.Api/Constants/Policies.cs             |  2 +-
 Jellyfin.Api/Controllers/ImageController.cs    |  1 +
 Jellyfin.Api/Controllers/SystemController.cs   |  2 +-
 .../StartupDtos/StartupConfigurationDto.cs     |  2 +-
 .../ApiServiceCollectionExtensions.cs          |  8 ++++----
 12 files changed, 38 insertions(+), 32 deletions(-)
 rename Jellyfin.Api/Auth/{IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs => FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs} (73%)
 create mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
 delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs

diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
similarity index 73%
rename from Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
rename to Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
index 6c9258b3dc..2a02f8bc71 100644
--- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
@@ -6,23 +6,23 @@ using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 
-namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
+namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
 {
     /// <summary>
-    /// Escape schedule controls handler.
+    /// Ignore parental control schedule and allow before startup wizard has been completed.
     /// </summary>
-    public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
+    public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
     {
         private readonly IConfigurationManager _configurationManager;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="IgnoreParentalControlOrFirstTimeSetupHandler"/> class.
+        /// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
         /// </summary>
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
-        public IgnoreParentalControlOrFirstTimeSetupHandler(
+        public FirstTimeOrIgnoreParentalControlSetupHandler(
             IUserManager userManager,
             INetworkManager networkManager,
             IHttpContextAccessor httpContextAccessor,
@@ -35,8 +35,14 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
         /// <inheritdoc />
         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
         {
+            if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            {
+                context.Succeed(requirement);
+                return Task.CompletedTask;
+            }
+
             var validated = ValidateClaims(context.User, ignoreSchedule: true);
-            if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            if (validated)
             {
                 context.Succeed(requirement);
             }
diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
new file mode 100644
index 0000000000..00aaec334b
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
+{
+    /// <summary>
+    /// First time setup or ignore parental controls requirement.
+    /// </summary>
+    public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
+    {
+    }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
index 67fb2b79a1..9815e252ee 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
 namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
 {
     /// <summary>
-    /// Authorization handler for requiring first time setup or elevated privileges.
+    /// Authorization handler for requiring first time setup or default privileges.
     /// </summary>
     public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
     {
@@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
         }
 
         /// <inheritdoc />
-        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement)
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
         {
             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
             {
-                context.Succeed(firstTimeSetupOrElevatedRequirement);
+                context.Succeed(firstTimeSetupOrDefaultRequirement);
                 return Task.CompletedTask;
             }
 
             var validated = ValidateClaims(context.User);
             if (validated)
             {
-                context.Succeed(firstTimeSetupOrElevatedRequirement);
+                context.Succeed(firstTimeSetupOrDefaultRequirement);
             }
             else
             {
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
index 23d7ee01f3..f7366bd7a9 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Authorization;
 namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
 {
     /// <summary>
-    /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
+    /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
     /// </summary>
     public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
     {
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
deleted file mode 100644
index 36ded06250..0000000000
--- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
-{
-    /// <summary>
-    /// Escape schedule controls requirement.
-    /// </summary>
-    public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement
-    {
-    }
-}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
index d9ab8aa687..14722aa57e 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
 namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
 {
     /// <summary>
-    /// Local access handler.
+    /// Local access or require elevated privileges handler.
     /// </summary>
     public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
     {
@@ -30,7 +30,6 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
         {
             var validated = ValidateClaims(context.User, localAccessOnly: true);
-
             if (validated || context.User.IsInRole(UserRoles.Administrator))
             {
                 context.Succeed(requirement);
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
index ad96caa811..d9c64d01c4 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
@@ -3,7 +3,7 @@
 namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
 {
     /// <summary>
-    /// The local access authorization requirement.
+    /// The local access or elevated privileges authorization requirement.
     /// </summary>
     public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement
     {
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 8de637c4e9..7d77674700 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -48,6 +48,6 @@ namespace Jellyfin.Api.Constants
         /// <summary>
         /// Policy name for escaping schedule controls or requiring first time setup.
         /// </summary>
-        public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup";
+        public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
     }
 }
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 410456a25c..45447ae0cc 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -131,6 +131,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("Users/{userId}/Images/{itemType}")]
         [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 08f1b421db..bbfd163de5 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
         [HttpGet("Info")]
-        [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)]
+        [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<SystemInfo>> GetSystemInfo()
         {
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index 66e7976996..a5f012245a 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos
         /// <summary>
         /// Gets or sets UI language culture.
         /// </summary>
-        public string UICulture { get; set; } = null!;
+        public string? UICulture { get; set; }
 
         /// <summary>
         /// Gets or sets the metadata country code.
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 586746430a..83d8fac5b5 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -7,9 +7,9 @@ using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.DownloadPolicy;
+using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
-using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy;
 using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
 using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
@@ -47,7 +47,7 @@ namespace Jellyfin.Server.Extensions
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
-            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlOrFirstTimeSetupHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
@@ -89,11 +89,11 @@ namespace Jellyfin.Server.Extensions
                         policy.AddRequirements(new IgnoreParentalControlRequirement());
                     });
                 options.AddPolicy(
-                    Policies.IgnoreParentalControlOrFirstTimeSetup,
+                    Policies.FirstTimeSetupOrIgnoreParentalControl,
                     policy =>
                     {
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
-                        policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement());
+                        policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement());
                     });
                 options.AddPolicy(
                     Policies.LocalAccessOnly,

From 0cf75992a8cfbf0795ea3837a926c37ab7e4cbf2 Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Fri, 7 Aug 2020 11:55:22 +0200
Subject: [PATCH 447/463] Use MemoryCache.Set since SetValue does not flush to
 cache automatically.

---
 Emby.Server.Implementations/Library/LibraryManager.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 04b530fcea..7b770d9400 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            _memoryCache.CreateEntry(item.Id).SetValue(item);
+            _memoryCache.Set(item.Id, item);
         }
 
         public void DeleteItem(BaseItem item, DeleteOptions options)

From e735ab6cc0faaec61d6aceeb0b946ba94a8c103c Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Fri, 7 Aug 2020 06:39:20 -0600
Subject: [PATCH 448/463] Remove extra Required

---
 Jellyfin.Api/Controllers/NotificationsController.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 47ce48b2da..cf3e780b44 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -98,8 +98,8 @@ namespace Jellyfin.Api.Controllers
         public ActionResult CreateAdminNotification(
             [FromQuery] string? url,
             [FromQuery] NotificationLevel? level,
-            [FromQuery, Required] string name = "",
-            [FromQuery, Required] string description = "")
+            [FromQuery] string name = "",
+            [FromQuery] string description = "")
         {
             var notification = new NotificationRequest
             {

From bea519de5b78f03f44fe04a4231a63838a010594 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Fri, 7 Aug 2020 13:22:18 -0400
Subject: [PATCH 449/463] Fix MemoryCache Usage.

---
 Emby.Server.Implementations/Channels/ChannelManager.cs | 2 +-
 Emby.Server.Implementations/Devices/DeviceManager.cs   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 8d6292867b..d8ab1f1a14 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.Channels
             var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
                    .ConfigureAwait(false);
             var list = mediaInfo.ToList();
-            _memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
+            _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
 
             return list;
         }
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index 2921a7f0e0..cc4b407f5c 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Devices
 
             lock (_capabilitiesSyncLock)
             {
-                _memoryCache.CreateEntry(deviceId).SetValue(capabilities);
+                _memoryCache.Set(deviceId, capabilities);
                 _json.SerializeToFile(capabilities, path);
             }
         }

From 750d8a989fed4de7440c2ada9e1eaf5f51fe69d6 Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sat, 8 Aug 2020 14:22:14 -0400
Subject: [PATCH 450/463] Clean up LibraryChangedNotifier.

---
 .../EntryPoints/LibraryChangedNotifier.cs     | 93 ++++++++-----------
 1 file changed, 41 insertions(+), 52 deletions(-)

diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index c1068522a7..e838936682 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
     public class LibraryChangedNotifier : IServerEntryPoint
     {
         /// <summary>
-        /// The library manager.
+        /// The library update duration.
         /// </summary>
-        private readonly ILibraryManager _libraryManager;
+        private const int LibraryUpdateDuration = 30000;
 
+        private readonly ILibraryManager _libraryManager;
+        private readonly IProviderManager _providerManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
         private readonly ILogger<LibraryChangedNotifier> _logger;
@@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private readonly List<Folder> _foldersAddedTo = new List<Folder>();
         private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
-
         private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
         private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
         private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
-
-        /// <summary>
-        /// Gets or sets the library update timer.
-        /// </summary>
-        /// <value>The library update timer.</value>
-        private Timer LibraryUpdateTimer { get; set; }
-
-        /// <summary>
-        /// The library update duration.
-        /// </summary>
-        private const int LibraryUpdateDuration = 30000;
-
-        private readonly IProviderManager _providerManager;
+        private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
 
         public LibraryChangedNotifier(
             ILibraryManager libraryManager,
@@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
             _providerManager = providerManager;
         }
 
+        /// <summary>
+        /// Gets or sets the library update timer.
+        /// </summary>
+        /// <value>The library update timer.</value>
+        private Timer LibraryUpdateTimer { get; set; }
+
         public Task RunAsync()
         {
-            _libraryManager.ItemAdded += libraryManager_ItemAdded;
-            _libraryManager.ItemUpdated += libraryManager_ItemUpdated;
-            _libraryManager.ItemRemoved += libraryManager_ItemRemoved;
+            _libraryManager.ItemAdded += OnLibraryItemAdded;
+            _libraryManager.ItemUpdated += OnLibraryItemUpdated;
+            _libraryManager.ItemRemoved += OnLibraryItemRemoved;
 
-            _providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
-            _providerManager.RefreshStarted += _providerManager_RefreshStarted;
-            _providerManager.RefreshProgress += _providerManager_RefreshProgress;
+            _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
+            _providerManager.RefreshStarted += OnProviderRefreshStarted;
+            _providerManager.RefreshProgress += OnProviderRefreshProgress;
 
             return Task.CompletedTask;
         }
 
-        private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
-
-        private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+        private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
         {
             var item = e.Argument.Item1;
 
@@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
 
             foreach (var collectionFolder in collectionFolders)
             {
-                var collectionFolderDict = new Dictionary<string, string>();
-                collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
-                collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
+                var collectionFolderDict = new Dictionary<string, string>
+                {
+                    ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+                    ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
+                };
 
                 try
                 {
@@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
-        private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
+        private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
         {
-            _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
+            OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
         }
 
-        private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
+        private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
         {
-            _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
+            OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
         }
 
         private static bool EnableRefreshMessage(BaseItem item)
         {
-            var folder = item as Folder;
-
-            if (folder == null)
+            if (!(item is Folder folder))
             {
                 return false;
             }
@@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+        void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {
@@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                 }
 
-                var parent = e.Item.GetParent() as Folder;
-                if (parent != null)
+                if (e.Item.GetParent() is Folder parent)
                 {
                     _foldersAddedTo.Add(parent);
                 }
@@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {
@@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
             {
                 if (LibraryUpdateTimer == null)
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
-                                                   Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
                 }
                 else
                 {
@@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
+        void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {
@@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
             {
                 if (LibraryUpdateTimer == null)
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
-                                                   Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
                 }
                 else
                 {
                     LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
                 }
 
-                var parent = e.Parent as Folder;
-                if (parent != null)
+                if (e.Parent is Folder parent)
                 {
                     _foldersRemovedFrom.Add(parent);
                 }
@@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer = null;
                 }
 
-                _libraryManager.ItemAdded -= libraryManager_ItemAdded;
-                _libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
-                _libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
+                _libraryManager.ItemAdded -= OnLibraryItemAdded;
+                _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+                _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
 
-                _providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
-                _providerManager.RefreshStarted -= _providerManager_RefreshStarted;
-                _providerManager.RefreshProgress -= _providerManager_RefreshProgress;
+                _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+                _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+                _providerManager.RefreshProgress -= OnProviderRefreshProgress;
             }
         }
     }

From 3ea4d9303e2d83e6b7c2a934c302349b2997045e Mon Sep 17 00:00:00 2001
From: Mister Rajoy <danielarezdiaz@gmail.com>
Date: Sat, 8 Aug 2020 21:21:32 +0200
Subject: [PATCH 451/463] Fix Split versions

---
 Jellyfin.Api/Controllers/VideosController.cs | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index fe065c76f6..fafa722c57 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
@@ -170,6 +170,11 @@ namespace Jellyfin.Api.Controllers
                 return NotFound("The video either does not exist or the id does not belong to a video.");
             }
 
+            if (video.LinkedAlternateVersions.Length == 0)
+            {
+                video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
+            }
+
             foreach (var link in video.GetLinkedAlternateVersions())
             {
                 link.SetPrimaryVersionId(null);

From 542185fba45b11b1701cc53150e3e1ece5f09cc1 Mon Sep 17 00:00:00 2001
From: Andreas B <6439218+YouKnowBlom@users.noreply.github.com>
Date: Sun, 9 Aug 2020 16:44:46 +0200
Subject: [PATCH 452/463] Avoid including stray commas in HLS codecs field

---
 Jellyfin.Api/Controllers/DynamicHlsController.cs | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index f6f08e873c..d581ab8cd2 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1496,9 +1496,14 @@ namespace Jellyfin.Api.Controllers
 
             StringBuilder codecs = new StringBuilder();
 
-            codecs.Append(videoCodecs)
-                .Append(',')
-                .Append(audioCodecs);
+            codecs.Append(videoCodecs);
+
+            if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+            {
+                codecs.Append(',');
+            }
+
+            codecs.Append(audioCodecs);
 
             if (codecs.Length > 1)
             {

From 7462a0a9e8a422259fb6c64d93a8d561d1be067c Mon Sep 17 00:00:00 2001
From: Patrick Barron <barronpm@gmail.com>
Date: Sun, 9 Aug 2020 11:50:52 -0400
Subject: [PATCH 453/463] Make event methods private.

---
 .../EntryPoints/LibraryChangedNotifier.cs                     | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index e838936682..1deef7f720 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {
@@ -239,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
+        private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {

From fc01bdb91ddb14740a9f04d521370d06dd7bcf83 Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Sun, 9 Aug 2020 19:26:06 +0200
Subject: [PATCH 454/463] Use GetEncodedPathAndQuery since ASP.NET Request.Path
 does not contain query parameters

---
 Jellyfin.Api/Controllers/DashboardController.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index a7bdb24f6d..33abe3ccdc 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
@@ -202,10 +203,11 @@ namespace Jellyfin.Api.Controllers
             var path = resourceName;
             var basePath = WebClientUiPath;
 
+            var requestPathAndQuery = Request.GetEncodedPathAndQuery();
             // Bounce them to the startup wizard if it hasn't been completed yet
             if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
-                && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase)
-                && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase))
+                && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
+                && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
             {
                 return Redirect("index.html?start=wizard#!/wizardstart.html");
             }

From e7f55c5110adea7092b8965a1c0fc45bc7cd0453 Mon Sep 17 00:00:00 2001
From: crobibero <cody@robibe.ro>
Date: Sun, 9 Aug 2020 13:51:25 -0600
Subject: [PATCH 455/463] Fix Requirement assigned to Handler

---
 .../FirstTimeOrIgnoreParentalControlSetupHandler.cs          | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
index 2a02f8bc71..31482a930f 100644
--- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
@@ -1,5 +1,4 @@
 using System.Threading.Tasks;
-using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
@@ -11,7 +10,7 @@ namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
     /// <summary>
     /// Ignore parental control schedule and allow before startup wizard has been completed.
     /// </summary>
-    public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
+    public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
     {
         private readonly IConfigurationManager _configurationManager;
 
@@ -33,7 +32,7 @@ namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
         }
 
         /// <inheritdoc />
-        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
         {
             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
             {

From 1dcc678a6afef4075c264968708eb1b837a0d409 Mon Sep 17 00:00:00 2001
From: cvium <clausvium@gmail.com>
Date: Sun, 9 Aug 2020 22:59:31 +0200
Subject: [PATCH 456/463] Fix collages

---
 Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index b08c3750d7..10bb59648f 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -115,7 +115,7 @@ namespace Jellyfin.Drawing.Skia
 
                 // resize to the same aspect as the original
                 int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
-                using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
+                using var resizedImage = SkiaEncoder.ResizeImage(currentBitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
 
                 // crop image
                 int ix = Math.Abs((iWidth - iSlice) / 2);
@@ -177,7 +177,7 @@ namespace Jellyfin.Drawing.Skia
 
                     // Scale image. The FromBitmap creates a copy
                     var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
-                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo));
+                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
 
                     // draw this image into the strip at the next position
                     var xPos = x * cellWidth;

From c43721f27f29b9195ad8cf9a31c2780060a387ce Mon Sep 17 00:00:00 2001
From: Mario Michel <1108mario@gmail.com>
Date: Sun, 9 Aug 2020 21:17:59 +0000
Subject: [PATCH 457/463] Translated using Weblate (German) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/

---
 Emby.Server.Implementations/Localization/Core/de.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index eec8802082..fe4fbc6115 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -5,7 +5,7 @@
     "Artists": "Interpreten",
     "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
-    "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+    "CameraImageUploadedFrom": "Ein neues Kamera Foto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Sammlungen",
@@ -106,7 +106,7 @@
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
-    "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+    "TaskRefreshLibrary": "Scanne alle Media Bibliotheken",
     "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
     "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",

From f98e751d189097e38c73b0f3ffacb2b2cc9bc9df Mon Sep 17 00:00:00 2001
From: Alf Sebastian Houge <mr.coolsebastian@hotmail.com>
Date: Mon, 10 Aug 2020 11:03:48 +0200
Subject: [PATCH 458/463] Update README.md

Fix spelling mistake

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 5128e5aea2..55d6917ae0 100644
--- a/README.md
+++ b/README.md
@@ -167,4 +167,4 @@ switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
 
 Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
 
-**NOTE:** The setup wizard can not be run if the web client is hosted seperately.
+**NOTE:** The setup wizard can not be run if the web client is hosted separately.

From a6e6f378e14b5d887a1bd85522a7e76ad98b6ac7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2020 12:01:45 +0000
Subject: [PATCH 459/463] Bump Swashbuckle.AspNetCore.ReDoc from 5.3.3 to 5.5.1

Bumps [Swashbuckle.AspNetCore.ReDoc](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 5.3.3 to 5.5.1.
- [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)
- [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v5.3.3...v5.5.1)

Signed-off-by: dependabot[bot] <support@github.com>
---
 Jellyfin.Api/Jellyfin.Api.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index db20e82670..24bc07b666 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -18,7 +18,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
-    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
   </ItemGroup>
 
   <ItemGroup>

From 4fd2b7a879ab4595b125740d0bd094fed9523775 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2020 12:01:45 +0000
Subject: [PATCH 460/463] Bump Microsoft.NET.Test.Sdk from 16.6.1 to 16.7.0

Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.6.1 to 16.7.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v16.6.1...v16.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
---
 tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj              | 2 +-
 tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj        | 2 +-
 .../Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj  | 2 +-
 .../Jellyfin.MediaEncoding.Tests.csproj                         | 2 +-
 tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj        | 2 +-
 tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj      | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 4011e4aa8c..f77eba376b 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -17,7 +17,7 @@
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 4cb1da994e..7464740445 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -13,7 +13,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 18724f31c1..1559f70ab3 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -13,7 +13,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 646ef00fd0..e1a0895476 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -19,7 +19,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 1434cce96e..0e9e915632 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -13,7 +13,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
index 93bc8433af..a4ef10648b 100644
--- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -9,7 +9,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

From c65275b239b83bba261e9ba63bf782aeec5f1db3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2020 12:01:48 +0000
Subject: [PATCH 461/463] Bump TvDbSharper from 3.2.0 to 3.2.1

Bumps [TvDbSharper](https://github.com/HristoKolev/TvDbSharper) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/HristoKolev/TvDbSharper/releases)
- [Changelog](https://github.com/HristoKolev/TvDbSharper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/HristoKolev/TvDbSharper/compare/v3.2.0...v3.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
---
 MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 09879997a3..7c0b542509 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -20,7 +20,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
-    <PackageReference Include="TvDbSharper" Version="3.2.0" />
+    <PackageReference Include="TvDbSharper" Version="3.2.1" />
   </ItemGroup>
 
   <PropertyGroup>

From 40dc4472e3f4cd5ba31d93559058c0dc13a68c1f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2020 12:01:58 +0000
Subject: [PATCH 462/463] Bump ServiceStack.Text.Core from 5.9.0 to 5.9.2

Bumps [ServiceStack.Text.Core](https://github.com/ServiceStack/ServiceStack.Text) from 5.9.0 to 5.9.2.
- [Release notes](https://github.com/ServiceStack/ServiceStack.Text/releases)
- [Commits](https://github.com/ServiceStack/ServiceStack.Text/compare/v5.9...v5.9.2)

Signed-off-by: dependabot[bot] <support@github.com>
---
 Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index d3e212be13..1adef68aa7 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -38,7 +38,7 @@
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
     <PackageReference Include="Mono.Nat" Version="2.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
-    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
+    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
     <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.0.9" />

From 72176ab2ca3eda00d22bde8614891b12c8bc6b66 Mon Sep 17 00:00:00 2001
From: Moritz <moritz.leick@googlemail.com>
Date: Mon, 10 Aug 2020 18:33:56 +0000
Subject: [PATCH 463/463] Translated using Weblate (German) Translation:
 Jellyfin/Jellyfin Translate-URL:
 https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/

---
 Emby.Server.Implementations/Localization/Core/de.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index fe4fbc6115..fcbe9566e2 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -5,7 +5,7 @@
     "Artists": "Interpreten",
     "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
-    "CameraImageUploadedFrom": "Ein neues Kamera Foto wurde von {0} hochgeladen",
+    "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Sammlungen",
@@ -106,7 +106,7 @@
     "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
     "TaskCleanLogs": "Lösche Log Pfad",
     "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
-    "TaskRefreshLibrary": "Scanne alle Media Bibliotheken",
+    "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
     "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
     "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",