2
0
Эх сурвалжийг харах

Add SessionInfoDto, DeviceInfoDto and implement JsonDelimitedArrayConverter.Write

Shadowghost 8 сар өмнө
parent
commit
7a2427bf07

+ 129 - 8
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1,7 +1,5 @@
 #nullable disable
 
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session
         private Timer _inactiveTimer;
 
         private DtoOptions _itemInfoDtoOptions;
-        private bool _disposed = false;
+        private bool _disposed;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionManager"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param>
+        /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param>
+        /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+        /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param>
+        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+        /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+        /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
         public SessionManager(
             ILogger<SessionManager> logger,
             IEventManager eventManager,
             IUserDataManager userDataManager,
-            IServerConfigurationManager config,
+            IServerConfigurationManager serverConfigurationManager,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IMusicManager musicManager,
@@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session
             _logger = logger;
             _eventManager = eventManager;
             _userDataManager = userDataManager;
-            _config = config;
+            _config = serverConfigurationManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _musicManager = musicManager;
@@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session
                 deviceName = "Network Device";
             }
 
-            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
+            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
+            {
+                DeviceId = deviceId
+            };
             if (string.IsNullOrEmpty(deviceOptions.CustomName))
             {
                 sessionInfo.DeviceName = deviceName;
@@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session
             return session;
         }
 
+        private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
+        {
+            return new SessionInfoDto
+            {
+                PlayState = sessionInfo.PlayState,
+                AdditionalUsers = sessionInfo.AdditionalUsers,
+                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
+                RemoteEndPoint = sessionInfo.RemoteEndPoint,
+                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
+                Id = sessionInfo.Id,
+                UserId = sessionInfo.UserId,
+                UserName = sessionInfo.UserName,
+                Client = sessionInfo.Client,
+                LastActivityDate = sessionInfo.LastActivityDate,
+                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
+                LastPausedDate = sessionInfo.LastPausedDate,
+                DeviceName = sessionInfo.DeviceName,
+                DeviceType = sessionInfo.DeviceType,
+                NowPlayingItem = sessionInfo.NowPlayingItem,
+                NowViewingItem = sessionInfo.NowViewingItem,
+                DeviceId = sessionInfo.DeviceId,
+                ApplicationVersion = sessionInfo.ApplicationVersion,
+                TranscodingInfo = sessionInfo.TranscodingInfo,
+                IsActive = sessionInfo.IsActive,
+                SupportsMediaControl = sessionInfo.SupportsMediaControl,
+                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
+                NowPlayingQueue = sessionInfo.NowPlayingQueue,
+                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
+                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
+                PlaylistItemId = sessionInfo.PlaylistItemId,
+                ServerId = sessionInfo.ServerId,
+                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
+                SupportedCommands = sessionInfo.SupportedCommands
+            };
+        }
+
         /// <inheritdoc />
         public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
         {
@@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session
                     UserName = user.Username
                 };
 
-                session.AdditionalUsers = [..session.AdditionalUsers, newUser];
+                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
             }
         }
 
@@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session
             var returnResult = new AuthenticationResult
             {
                 User = _userManager.GetUserDto(user, request.RemoteEndPoint),
-                SessionInfo = session,
+                SessionInfo = ToSessionInfoDto(session),
                 AccessToken = token,
                 ServerId = _appHost.SystemId
             };
@@ -1800,6 +1853,74 @@ namespace Emby.Server.Implementations.Session
             return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
         }
 
+        /// <inheritdoc/>
+        public IReadOnlyList<SessionInfoDto> GetSessions(
+            Guid userId,
+            string deviceId,
+            int? activeWithinSeconds,
+            Guid? controllableUserToCheck)
+        {
+            var result = Sessions;
+            var user = _userManager.GetUserById(userId);
+            if (!string.IsNullOrEmpty(deviceId))
+            {
+                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+            }
+
+            if (!controllableUserToCheck.IsNullOrEmpty())
+            {
+                result = result.Where(i => i.SupportsRemoteControl);
+
+                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
+                if (controlledUser is null)
+                {
+                    return [];
+                }
+
+                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
+                {
+                    // Controlled user has device sharing disabled
+                    result = result.Where(i => !i.UserId.IsEmpty());
+                }
+
+                if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
+                {
+                    // User cannot control other user's sessions, validate user id.
+                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value));
+                }
+
+                result = result.Where(i =>
+                {
+                    if (!string.IsNullOrWhiteSpace(i.DeviceId) && !_deviceManager.CanAccessDevice(user, i.DeviceId))
+                    {
+                        return false;
+                    }
+
+                    return true;
+                });
+            }
+            else if (!user.HasPermission(PermissionKind.IsAdministrator))
+            {
+                // Request isn't from administrator, limit to "own" sessions.
+                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
+
+                // Don't report acceleration type for non-admin users.
+                result = result.Select(r =>
+                {
+                    r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
+                    return r;
+                });
+            }
+
+            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+            {
+                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+                result = result.Where(i => i.LastActivityDate >= minActiveDate);
+            }
+
+            return result.Select(ToSessionInfoDto).ToList();
+        }
+
         /// <inheritdoc />
         public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
         {

+ 4 - 6
Jellyfin.Api/Controllers/DevicesController.cs

@@ -1,15 +1,13 @@
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Dtos;
-using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Queries;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
     [HttpGet]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
+    public ActionResult<QueryResult<DeviceInfoDto>> GetDevices([FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
         return _deviceManager.GetDevicesForUser(userId);
@@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController
     [HttpGet("Info")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
+    public ActionResult<DeviceInfoDto> GetDeviceInfo([FromQuery, Required] string id)
     {
         var deviceInfo = _deviceManager.GetDevice(id);
         if (deviceInfo is null)
@@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController
     [HttpGet("Options")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
+    public ActionResult<DeviceOptionsDto> GetDeviceOptions([FromQuery, Required] string id)
     {
         var deviceInfo = _deviceManager.GetDeviceOptions(id);
         if (deviceInfo is null)

+ 12 - 73
Jellyfin.Api/Controllers/SessionController.cs

@@ -1,18 +1,13 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
-using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
-using Jellyfin.Api.Models.SessionDtos;
 using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
-using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
@@ -32,22 +27,18 @@ public class SessionController : BaseJellyfinApiController
 {
     private readonly ISessionManager _sessionManager;
     private readonly IUserManager _userManager;
-    private readonly IDeviceManager _deviceManager;
 
     /// <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="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
     public SessionController(
         ISessionManager sessionManager,
-        IUserManager userManager,
-        IDeviceManager deviceManager)
+        IUserManager userManager)
     {
         _sessionManager = sessionManager;
         _userManager = userManager;
-        _deviceManager = deviceManager;
     }
 
     /// <summary>
@@ -57,77 +48,25 @@ public class SessionController : BaseJellyfinApiController
     /// <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>
+    /// <returns>An <see cref="IReadOnlyList{SessionInfoDto}"/> with the available sessions.</returns>
     [HttpGet("Sessions")]
     [Authorize]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+    public ActionResult<IReadOnlyList<SessionInfoDto>> GetSessions(
         [FromQuery] Guid? controllableByUserId,
         [FromQuery] string? deviceId,
         [FromQuery] int? activeWithinSeconds)
     {
-        var result = _sessionManager.Sessions;
-        var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator);
-
-        if (!string.IsNullOrEmpty(deviceId))
-        {
-            result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
-        }
-
-        if (!controllableByUserId.IsNullOrEmpty())
+        Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId);
+        var result = _sessionManager.GetSessions(
+            User.GetUserId(),
+            deviceId,
+            activeWithinSeconds,
+            controllableUserToCheck);
+
+        if (result.Count == 0)
         {
-            result = result.Where(i => i.SupportsRemoteControl);
-
-            var user = _userManager.GetUserById(controllableByUserId.Value);
-            if (user is null)
-            {
-                return NotFound();
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
-            {
-                // User cannot control other user's sessions, validate user id.
-                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId)));
-            }
-
-            if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
-            {
-                result = result.Where(i => !i.UserId.IsEmpty());
-            }
-
-            result = result.Where(i =>
-            {
-                if (!string.IsNullOrWhiteSpace(i.DeviceId))
-                {
-                    if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
-                    {
-                        return false;
-                    }
-                }
-
-                return true;
-            });
-        }
-        else if (!isRequestingFromAdmin)
-        {
-            // Request isn't from administrator, limit to "own" sessions.
-            result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId()));
-        }
-
-        if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
-        {
-            var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
-            result = result.Where(i => i.LastActivityDate >= minActiveDate);
-        }
-
-        // Request isn't from administrator, don't report acceleration type.
-        if (!isRequestingFromAdmin)
-        {
-            result = result.Select(r =>
-            {
-                r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
-                return r;
-            });
+            return NotFound();
         }
 
         return Ok(result);

+ 16 - 17
Jellyfin.Data/Dtos/DeviceOptionsDto.cs

@@ -1,23 +1,22 @@
-namespace Jellyfin.Data.Dtos
+namespace Jellyfin.Data.Dtos;
+
+/// <summary>
+/// A dto representing custom options for a device.
+/// </summary>
+public class DeviceOptionsDto
 {
     /// <summary>
-    /// A dto representing custom options for a device.
+    /// Gets or sets the id.
     /// </summary>
-    public class DeviceOptionsDto
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        public int Id { get; set; }
+    public int Id { get; set; }
 
-        /// <summary>
-        /// Gets or sets the device id.
-        /// </summary>
-        public string? DeviceId { get; set; }
+    /// <summary>
+    /// Gets or sets the device id.
+    /// </summary>
+    public string? DeviceId { get; set; }
 
-        /// <summary>
-        /// Gets or sets the custom name.
-        /// </summary>
-        public string? CustomName { get; set; }
-    }
+    /// <summary>
+    /// Gets or sets the custom name.
+    /// </summary>
+    public string? CustomName { get; set; }
 }

+ 68 - 17
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Data.Dtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Enums;
@@ -13,6 +14,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using Microsoft.EntityFrameworkCore;
@@ -68,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Devices
         }
 
         /// <inheritdoc />
-        public async Task UpdateDeviceOptions(string deviceId, string deviceName)
+        public async Task UpdateDeviceOptions(string deviceId, string? deviceName)
         {
             DeviceOptions? deviceOptions;
             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -105,29 +107,37 @@ namespace Jellyfin.Server.Implementations.Devices
         }
 
         /// <inheritdoc />
-        public DeviceOptions GetDeviceOptions(string deviceId)
+        public DeviceOptionsDto? GetDeviceOptions(string deviceId)
         {
-            _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
+            if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions))
+            {
+                return ToDeviceOptionsDto(deviceOptions);
+            }
 
-            return deviceOptions ?? new DeviceOptions(deviceId);
+            return null;
         }
 
         /// <inheritdoc />
-        public ClientCapabilities GetCapabilities(string deviceId)
+        public ClientCapabilities GetCapabilities(string? deviceId)
         {
+            if (deviceId is null)
+            {
+                return new();
+            }
+
             return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result)
                 ? result
-                : new ClientCapabilities();
+                : new();
         }
 
         /// <inheritdoc />
-        public DeviceInfo? GetDevice(string id)
+        public DeviceInfoDto? GetDevice(string id)
         {
             var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
             _deviceOptions.TryGetValue(id, out var deviceOption);
 
             var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
-            return deviceInfo;
+            return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo);
         }
 
         /// <inheritdoc />
@@ -166,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Devices
         }
 
         /// <inheritdoc />
-        public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
+        public QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId)
         {
             IEnumerable<Device> devices = _devices.Values
                 .OrderByDescending(d => d.DateLastActivity)
@@ -187,9 +197,11 @@ namespace Jellyfin.Server.Implementations.Devices
                 {
                     _deviceOptions.TryGetValue(device.DeviceId, out var option);
                     return ToDeviceInfo(device, option);
-                }).ToArray();
+                })
+                .Select(ToDeviceInfoDto)
+                .ToArray();
 
-            return new QueryResult<DeviceInfo>(array);
+            return new QueryResult<DeviceInfoDto>(array);
         }
 
         /// <inheritdoc />
@@ -235,13 +247,9 @@ namespace Jellyfin.Server.Implementations.Devices
         private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
         {
             var caps = GetCapabilities(authInfo.DeviceId);
-            var user = _userManager.GetUserById(authInfo.UserId);
-            if (user is null)
-            {
-                throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
-            }
+            var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
 
-            return new DeviceInfo
+            return new()
             {
                 AppName = authInfo.AppName,
                 AppVersion = authInfo.AppVersion,
@@ -254,5 +262,48 @@ namespace Jellyfin.Server.Implementations.Devices
                 CustomName = options?.CustomName,
             };
         }
+
+        private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options)
+        {
+            return new()
+            {
+                Id = options.Id,
+                DeviceId = options.DeviceId,
+                CustomName = options.CustomName,
+            };
+        }
+
+        private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info)
+        {
+            return new()
+            {
+                Name = info.Name,
+                CustomName = info.CustomName,
+                AccessToken = info.AccessToken,
+                Id = info.Id,
+                LastUserName = info.LastUserName,
+                AppName = info.AppName,
+                AppVersion = info.AppVersion,
+                LastUserId = info.LastUserId,
+                DateLastActivity = info.DateLastActivity,
+                Capabilities = ToClientCapabilitiesDto(info.Capabilities),
+                IconUrl = info.IconUrl
+            };
+        }
+
+        /// <inheritdoc />
+        public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities)
+        {
+            return new()
+            {
+                PlayableMediaTypes = capabilities.PlayableMediaTypes,
+                SupportedCommands = capabilities.SupportedCommands,
+                SupportsMediaControl = capabilities.SupportsMediaControl,
+                SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier,
+                DeviceProfile = capabilities.DeviceProfile,
+                AppStoreUrl = capabilities.AppStoreUrl,
+                IconUrl = capabilities.IconUrl
+            };
+        }
     }
 }

+ 22 - 11
MediaBrowser.Controller/Authentication/AuthenticationResult.cs

@@ -1,20 +1,31 @@
 #nullable disable
 
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 
-namespace MediaBrowser.Controller.Authentication
+namespace MediaBrowser.Controller.Authentication;
+
+/// <summary>
+/// A class representing an authentication result.
+/// </summary>
+public class AuthenticationResult
 {
-    public class AuthenticationResult
-    {
-        public UserDto User { get; set; }
+    /// <summary>
+    /// Gets or sets the user.
+    /// </summary>
+    public UserDto User { get; set; }
 
-        public SessionInfo SessionInfo { get; set; }
+    /// <summary>
+    /// Gets or sets the session info.
+    /// </summary>
+    public SessionInfoDto SessionInfo { get; set; }
 
-        public string AccessToken { get; set; }
+    /// <summary>
+    /// Gets or sets the access token.
+    /// </summary>
+    public string AccessToken { get; set; }
 
-        public string ServerId { get; set; }
-    }
+    /// <summary>
+    /// Gets or sets the server id.
+    /// </summary>
+    public string ServerId { get; set; }
 }

+ 93 - 57
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -1,81 +1,117 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
 using System;
 using System.Threading.Tasks;
+using Jellyfin.Data.Dtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Queries;
 using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 
-namespace MediaBrowser.Controller.Devices
+namespace MediaBrowser.Controller.Devices;
+
+/// <summary>
+/// Device manager interface.
+/// </summary>
+public interface IDeviceManager
 {
-    public interface IDeviceManager
-    {
-        event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+    /// <summary>
+    /// Event handler for updated device options.
+    /// </summary>
+    event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+
+    /// <summary>
+    /// Creates a new device.
+    /// </summary>
+    /// <param name="device">The device to create.</param>
+    /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
+    Task<Device> CreateDevice(Device device);
 
-        /// <summary>
-        /// Creates a new device.
-        /// </summary>
-        /// <param name="device">The device to create.</param>
-        /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
-        Task<Device> CreateDevice(Device device);
+    /// <summary>
+    /// Saves the capabilities.
+    /// </summary>
+    /// <param name="deviceId">The device id.</param>
+    /// <param name="capabilities">The capabilities.</param>
+    void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
 
-        /// <summary>
-        /// Saves the capabilities.
-        /// </summary>
-        /// <param name="deviceId">The device id.</param>
-        /// <param name="capabilities">The capabilities.</param>
-        void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
+    /// <summary>
+    /// Gets the capabilities.
+    /// </summary>
+    /// <param name="deviceId">The device id.</param>
+    /// <returns>ClientCapabilities.</returns>
+    ClientCapabilities GetCapabilities(string? deviceId);
 
-        /// <summary>
-        /// Gets the capabilities.
-        /// </summary>
-        /// <param name="deviceId">The device id.</param>
-        /// <returns>ClientCapabilities.</returns>
-        ClientCapabilities GetCapabilities(string deviceId);
+    /// <summary>
+    /// Gets the device information.
+    /// </summary>
+    /// <param name="id">The identifier.</param>
+    /// <returns>DeviceInfoDto.</returns>
+    DeviceInfoDto? GetDevice(string id);
 
-        /// <summary>
-        /// Gets the device information.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>DeviceInfo.</returns>
-        DeviceInfo GetDevice(string id);
+    /// <summary>
+    /// Gets devices based on the provided query.
+    /// </summary>
+    /// <param name="query">The device query.</param>
+    /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
+    QueryResult<Device> GetDevices(DeviceQuery query);
 
-        /// <summary>
-        /// Gets devices based on the provided query.
-        /// </summary>
-        /// <param name="query">The device query.</param>
-        /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
-        QueryResult<Device> GetDevices(DeviceQuery query);
+    /// <summary>
+    /// Gets device infromation based on the provided query.
+    /// </summary>
+    /// <param name="query">The device query.</param>
+    /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns>
+    QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
 
-        QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
+    /// <summary>
+    /// Gets the device information.
+    /// </summary>
+    /// <param name="userId">The user's id, or <c>null</c>.</param>
+    /// <returns>IEnumerable&lt;DeviceInfoDto&gt;.</returns>
+    QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId);
 
-        /// <summary>
-        /// Gets the devices.
-        /// </summary>
-        /// <param name="userId">The user's id, or <c>null</c>.</param>
-        /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
-        QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
+    /// <summary>
+    /// Deletes a device.
+    /// </summary>
+    /// <param name="device">The device.</param>
+    /// <returns>A <see cref="Task"/> representing the deletion of the device.</returns>
+    Task DeleteDevice(Device device);
 
-        Task DeleteDevice(Device device);
+    /// <summary>
+    /// Updates a device.
+    /// </summary>
+    /// <param name="device">The device.</param>
+    /// <returns>A <see cref="Task"/> representing the update of the device.</returns>
+    Task UpdateDevice(Device device);
 
-        Task UpdateDevice(Device device);
+    /// <summary>
+    /// Determines whether this instance [can access device] the specified user identifier.
+    /// </summary>
+    /// <param name="user">The user to test.</param>
+    /// <param name="deviceId">The device id to test.</param>
+    /// <returns>Whether the user can access the device.</returns>
+    bool CanAccessDevice(User user, string deviceId);
 
-        /// <summary>
-        /// Determines whether this instance [can access device] the specified user identifier.
-        /// </summary>
-        /// <param name="user">The user to test.</param>
-        /// <param name="deviceId">The device id to test.</param>
-        /// <returns>Whether the user can access the device.</returns>
-        bool CanAccessDevice(User user, string deviceId);
+    /// <summary>
+    /// Updates the options of a device.
+    /// </summary>
+    /// <param name="deviceId">The device id.</param>
+    /// <param name="deviceName">The device name.</param>
+    /// <returns>A <see cref="Task"/> representing the update of the device options.</returns>
+    Task UpdateDeviceOptions(string deviceId, string? deviceName);
 
-        Task UpdateDeviceOptions(string deviceId, string deviceName);
+    /// <summary>
+    /// Gets the options of a device.
+    /// </summary>
+    /// <param name="deviceId">The device id.</param>
+    /// <returns><see cref="DeviceOptions"/> of the device.</returns>
+    DeviceOptionsDto? GetDeviceOptions(string deviceId);
 
-        DeviceOptions GetDeviceOptions(string deviceId);
-    }
+    /// <summary>
+    /// Gets the dto for client capabilites.
+    /// </summary>
+    /// <param name="capabilities">The client capabilities.</param>
+    /// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns>
+    ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities);
 }

+ 1 - 2
MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs

@@ -1,6 +1,5 @@
 using System;
 using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Controller.Events.Authentication;
@@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs
     /// <summary>
     /// Gets or sets the session information.
     /// </summary>
-    public SessionInfo? SessionInfo { get; set; }
+    public SessionInfoDto? SessionInfo { get; set; }
 
     /// <summary>
     /// Gets or sets the server id.

+ 3 - 2
MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs

@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Session;
 
 namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
@@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 /// <summary>
 /// Sessions message.
 /// </summary>
-public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>>
+public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfoDto>>
 {
     /// <summary>
     /// Initializes a new instance of the <see cref="SessionsMessage"/> class.
     /// </summary>
     /// <param name="data">Session info.</param>
-    public SessionsMessage(IReadOnlyList<SessionInfo> data)
+    public SessionsMessage(IReadOnlyList<SessionInfoDto> data)
         : base(data)
     {
     }

+ 11 - 0
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Entities.Security;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.SyncPlay;
 
@@ -292,6 +293,16 @@ namespace MediaBrowser.Controller.Session
         /// <returns>SessionInfo.</returns>
         SessionInfo GetSession(string deviceId, string client, string version);
 
+        /// <summary>
+        /// Gets all sessions available to a user.
+        /// </summary>
+        /// <param name="userId">The session identifier.</param>
+        /// <param name="deviceId">The device id.</param>
+        /// <param name="activeWithinSeconds">Active within session limit.</param>
+        /// <param name="controllableUserToCheck">Filter for sessions remote controllable for this user.</param>
+        /// <returns>IReadOnlyList{SessionInfoDto}.</returns>
+        IReadOnlyList<SessionInfoDto> GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck);
+
         /// <summary>
         /// Gets the session by authentication token.
         /// </summary>

+ 103 - 17
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -1,7 +1,5 @@
 #nullable disable
 
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session
         private readonly ISessionManager _sessionManager;
         private readonly ILogger _logger;
 
-        private readonly object _progressLock = new object();
+        private readonly object _progressLock = new();
         private Timer _progressTimer;
         private PlaybackProgressInfo _lastProgressInfo;
 
-        private bool _disposed = false;
+        private bool _disposed;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SessionInfo"/> class.
+        /// </summary>
+        /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+        /// <param name="logger">Instance of <see cref="ILogger"/> interface.</param>
         public SessionInfo(ISessionManager sessionManager, ILogger logger)
         {
             _sessionManager = sessionManager;
             _logger = logger;
 
-            AdditionalUsers = Array.Empty<SessionUserInfo>();
+            AdditionalUsers = [];
             PlayState = new PlayerStateInfo();
-            SessionControllers = Array.Empty<ISessionController>();
-            NowPlayingQueue = Array.Empty<QueueItem>();
-            NowPlayingQueueFullItems = Array.Empty<BaseItemDto>();
+            SessionControllers = [];
+            NowPlayingQueue = [];
+            NowPlayingQueueFullItems = [];
         }
 
+        /// <summary>
+        /// Gets or sets the play state.
+        /// </summary>
+        /// <value>The play state.</value>
         public PlayerStateInfo PlayState { get; set; }
 
-        public SessionUserInfo[] AdditionalUsers { get; set; }
+        /// <summary>
+        /// Gets or sets the additional users.
+        /// </summary>
+        /// <value>The additional users.</value>
+        public IReadOnlyList<SessionUserInfo> AdditionalUsers { get; set; }
 
+        /// <summary>
+        /// Gets or sets the client capabilities.
+        /// </summary>
+        /// <value>The client capabilities.</value>
         public ClientCapabilities Capabilities { get; set; }
 
         /// <summary>
@@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session
             {
                 if (Capabilities is null)
                 {
-                    return Array.Empty<MediaType>();
+                    return [];
                 }
 
                 return Capabilities.PlayableMediaTypes;
@@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session
         /// <value>The now playing item.</value>
         public BaseItemDto NowPlayingItem { get; set; }
 
+        /// <summary>
+        /// Gets or sets the now playing queue full items.
+        /// </summary>
+        /// <value>The now playing queue full items.</value>
         [JsonIgnore]
         public BaseItem FullNowPlayingItem { get; set; }
 
+        /// <summary>
+        /// Gets or sets the now viewing item.
+        /// </summary>
+        /// <value>The now viewing item.</value>
         public BaseItemDto NowViewingItem { get; set; }
 
         /// <summary>
@@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session
         /// </summary>
         /// <value>The session controller.</value>
         [JsonIgnore]
-        public ISessionController[] SessionControllers { get; set; }
+        public IReadOnlyList<ISessionController> SessionControllers { get; set; }
 
+        /// <summary>
+        /// Gets or sets the transcoding info.
+        /// </summary>
+        /// <value>The transcoding info.</value>
         public TranscodingInfo TranscodingInfo { get; set; }
 
         /// <summary>
@@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session
                     }
                 }
 
-                if (controllers.Length > 0)
+                if (controllers.Count > 0)
                 {
                     return false;
                 }
@@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the session supports media control.
+        /// </summary>
+        /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value>
         public bool SupportsMediaControl
         {
             get
@@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the session supports remote control.
+        /// </summary>
+        /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value>
         public bool SupportsRemoteControl
         {
             get
@@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        /// <summary>
+        /// Gets or sets the now playing queue.
+        /// </summary>
+        /// <value>The now playing queue.</value>
         public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; }
 
+        /// <summary>
+        /// Gets or sets the now playing queue full items.
+        /// </summary>
+        /// <value>The now playing queue full items.</value>
         public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the session has a custom device name.
+        /// </summary>
+        /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
         public bool HasCustomDeviceName { get; set; }
 
+        /// <summary>
+        /// Gets or sets the playlist item id.
+        /// </summary>
+        /// <value>The splaylist item id.</value>
         public string PlaylistItemId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the server id.
+        /// </summary>
+        /// <value>The server id.</value>
         public string ServerId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the user primary image tag.
+        /// </summary>
+        /// <value>The user primary image tag.</value>
         public string UserPrimaryImageTag { get; set; }
 
         /// <summary>
@@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session
         /// </summary>
         /// <value>The supported commands.</value>
         public IReadOnlyList<GeneralCommandType> SupportedCommands
-            => Capabilities is null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
+            => Capabilities is null ? [] : Capabilities.SupportedCommands;
 
+        /// <summary>
+        /// Ensures a controller of type exists.
+        /// </summary>
+        /// <typeparam name="T">Class to register.</typeparam>
+        /// <param name="factory">The factory.</param>
+        /// <returns>Tuple{ISessionController, bool}.</returns>
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {
             var controllers = SessionControllers.ToList();
@@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session
             }
 
             var newController = factory(this);
-            _logger.LogDebug("Creating new {0}", newController.GetType().Name);
+            _logger.LogDebug("Creating new {Factory}", newController.GetType().Name);
             controllers.Add(newController);
 
-            SessionControllers = controllers.ToArray();
+            SessionControllers = [.. controllers];
             return new Tuple<ISessionController, bool>(newController, true);
         }
 
+        /// <summary>
+        /// Adds a controller to the session.
+        /// </summary>
+        /// <param name="controller">The controller.</param>
         public void AddController(ISessionController controller)
         {
-            SessionControllers = [..SessionControllers, controller];
+            SessionControllers = [.. SessionControllers, controller];
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the session contains a user.
+        /// </summary>
+        /// <param name="userId">The user id to check.</param>
+        /// <returns><c>true</c> if this session contains the user; otherwise, <c>false</c>.</returns>
         public bool ContainsUser(Guid userId)
         {
             if (UserId.Equals(userId))
@@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session
             return false;
         }
 
+        /// <summary>
+        /// Starts automatic progressing.
+        /// </summary>
+        /// <param name="progressInfo">The playback progress info.</param>
+        /// <value>The supported commands.</value>
         public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
         {
             if (_disposed)
@@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        /// <summary>
+        /// Stops automatic progressing.
+        /// </summary>
         public void StopAutomaticProgress()
         {
             lock (_progressLock)
@@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        /// <summary>
+        /// Disposes the instance async.
+        /// </summary>
+        /// <returns>ValueTask.</returns>
         public async ValueTask DisposeAsync()
         {
             _disposed = true;
@@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session
             StopAutomaticProgress();
 
             var controllers = SessionControllers.ToList();
-            SessionControllers = Array.Empty<ISessionController>();
+            SessionControllers = [];
 
             foreach (var controller in controllers)
             {

+ 67 - 52
MediaBrowser.Model/Devices/DeviceInfo.cs

@@ -1,69 +1,84 @@
-#nullable disable
-#pragma warning disable CS1591
-
 using System;
 using MediaBrowser.Model.Session;
 
-namespace MediaBrowser.Model.Devices
+namespace MediaBrowser.Model.Devices;
+
+/// <summary>
+/// A class for device Information.
+/// </summary>
+public class DeviceInfo
 {
-    public class DeviceInfo
+    /// <summary>
+    /// Initializes a new instance of the <see cref="DeviceInfo"/> class.
+    /// </summary>
+    public DeviceInfo()
     {
-        public DeviceInfo()
-        {
-            Capabilities = new ClientCapabilities();
-        }
+        Capabilities = new ClientCapabilities();
+    }
 
-        public string Name { get; set; }
+    /// <summary>
+    /// Gets or sets the name.
+    /// </summary>
+    /// <value>The name.</value>
+    public string? Name { get; set; }
 
-        public string CustomName { get; set; }
+    /// <summary>
+    /// Gets or sets the custom name.
+    /// </summary>
+    /// <value>The custom name.</value>
+    public string? CustomName { get; set; }
 
-        /// <summary>
-        /// Gets or sets the access token.
-        /// </summary>
-        public string AccessToken { get; set; }
+    /// <summary>
+    /// Gets or sets the access token.
+    /// </summary>
+    /// <value>The access token.</value>
+    public string? AccessToken { get; set; }
 
-        /// <summary>
-        /// Gets or sets the identifier.
-        /// </summary>
-        /// <value>The identifier.</value>
-        public string Id { get; set; }
+    /// <summary>
+    /// Gets or sets the identifier.
+    /// </summary>
+    /// <value>The identifier.</value>
+    public string? Id { get; set; }
 
-        /// <summary>
-        /// Gets or sets the last name of the user.
-        /// </summary>
-        /// <value>The last name of the user.</value>
-        public string LastUserName { get; set; }
+    /// <summary>
+    /// Gets or sets the last name of the user.
+    /// </summary>
+    /// <value>The last name of the user.</value>
+    public string? LastUserName { get; set; }
 
-        /// <summary>
-        /// Gets or sets the name of the application.
-        /// </summary>
-        /// <value>The name of the application.</value>
-        public string AppName { get; set; }
+    /// <summary>
+    /// Gets or sets the name of the application.
+    /// </summary>
+    /// <value>The name of the application.</value>
+    public string? AppName { get; set; }
 
-        /// <summary>
-        /// Gets or sets the application version.
-        /// </summary>
-        /// <value>The application version.</value>
-        public string AppVersion { get; set; }
+    /// <summary>
+    /// Gets or sets the application version.
+    /// </summary>
+    /// <value>The application version.</value>
+    public string? AppVersion { get; set; }
 
-        /// <summary>
-        /// Gets or sets the last user identifier.
-        /// </summary>
-        /// <value>The last user identifier.</value>
-        public Guid LastUserId { get; set; }
+    /// <summary>
+    /// Gets or sets the last user identifier.
+    /// </summary>
+    /// <value>The last user identifier.</value>
+    public Guid? LastUserId { get; set; }
 
-        /// <summary>
-        /// Gets or sets the date last modified.
-        /// </summary>
-        /// <value>The date last modified.</value>
-        public DateTime DateLastActivity { get; set; }
+    /// <summary>
+    /// Gets or sets the date last modified.
+    /// </summary>
+    /// <value>The date last modified.</value>
+    public DateTime? DateLastActivity { get; set; }
 
-        /// <summary>
-        /// Gets or sets the capabilities.
-        /// </summary>
-        /// <value>The capabilities.</value>
-        public ClientCapabilities Capabilities { get; set; }
+    /// <summary>
+    /// Gets or sets the capabilities.
+    /// </summary>
+    /// <value>The capabilities.</value>
+    public ClientCapabilities Capabilities { get; set; }
 
-        public string IconUrl { get; set; }
-    }
+    /// <summary>
+    /// Gets or sets the icon URL.
+    /// </summary>
+    /// <value>The icon URL.</value>
+    public string? IconUrl { get; set; }
 }

+ 3 - 17
Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs → MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs

@@ -1,13 +1,11 @@
-using System;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Session;
 
-namespace Jellyfin.Api.Models.SessionDtos;
+namespace MediaBrowser.Model.Dto;
 
 /// <summary>
 /// Client capabilities dto.
@@ -18,13 +16,13 @@ public class ClientCapabilitiesDto
     /// Gets or sets the list of playable media types.
     /// </summary>
     [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
-    public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = Array.Empty<MediaType>();
+    public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
 
     /// <summary>
     /// Gets or sets the list of supported commands.
     /// </summary>
     [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
-    public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
+    public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
 
     /// <summary>
     /// Gets or sets a value indicating whether session supports media control.
@@ -51,18 +49,6 @@ public class ClientCapabilitiesDto
     /// </summary>
     public string? IconUrl { get; set; }
 
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-    // TODO: Remove after 10.9
-    [Obsolete("Unused")]
-    [DefaultValue(false)]
-    public bool? SupportsContentUploading { get; set; } = false;
-
-    // TODO: Remove after 10.9
-    [Obsolete("Unused")]
-    [DefaultValue(false)]
-    public bool? SupportsSync { get; set; } = false;
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-
     /// <summary>
     /// Convert the dto to the full <see cref="ClientCapabilities"/> model.
     /// </summary>

+ 83 - 0
MediaBrowser.Model/Dto/DeviceInfoDto.cs

@@ -0,0 +1,83 @@
+using System;
+
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// A DTO representing device information.
+/// </summary>
+public class DeviceInfoDto
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="DeviceInfoDto"/> class.
+    /// </summary>
+    public DeviceInfoDto()
+    {
+        Capabilities = new ClientCapabilitiesDto();
+    }
+
+    /// <summary>
+    /// Gets or sets the name.
+    /// </summary>
+    /// <value>The name.</value>
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// Gets or sets the custom name.
+    /// </summary>
+    /// <value>The custom name.</value>
+    public string? CustomName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the access token.
+    /// </summary>
+    /// <value>The access token.</value>
+    public string? AccessToken { get; set; }
+
+    /// <summary>
+    /// Gets or sets the identifier.
+    /// </summary>
+    /// <value>The identifier.</value>
+    public string? Id { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last name of the user.
+    /// </summary>
+    /// <value>The last name of the user.</value>
+    public string? LastUserName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the name of the application.
+    /// </summary>
+    /// <value>The name of the application.</value>
+    public string? AppName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the application version.
+    /// </summary>
+    /// <value>The application version.</value>
+    public string? AppVersion { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last user identifier.
+    /// </summary>
+    /// <value>The last user identifier.</value>
+    public Guid? LastUserId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the date last modified.
+    /// </summary>
+    /// <value>The date last modified.</value>
+    public DateTime? DateLastActivity { get; set; }
+
+    /// <summary>
+    /// Gets or sets the capabilities.
+    /// </summary>
+    /// <value>The capabilities.</value>
+    public ClientCapabilitiesDto Capabilities { get; set; }
+
+    /// <summary>
+    /// Gets or sets the icon URL.
+    /// </summary>
+    /// <value>The icon URL.</value>
+    public string? IconUrl { get; set; }
+}

+ 186 - 0
MediaBrowser.Model/Dto/SessionInfoDto.cs

@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// Session info DTO.
+/// </summary>
+public class SessionInfoDto
+{
+    /// <summary>
+    /// Gets or sets the play state.
+    /// </summary>
+    /// <value>The play state.</value>
+    public PlayerStateInfo? PlayState { get; set; }
+
+    /// <summary>
+    /// Gets or sets the additional users.
+    /// </summary>
+    /// <value>The additional users.</value>
+    public IReadOnlyList<SessionUserInfo>? AdditionalUsers { get; set; }
+
+    /// <summary>
+    /// Gets or sets the client capabilities.
+    /// </summary>
+    /// <value>The client capabilities.</value>
+    public ClientCapabilitiesDto? Capabilities { get; set; }
+
+    /// <summary>
+    /// Gets or sets the remote end point.
+    /// </summary>
+    /// <value>The remote end point.</value>
+    public string? RemoteEndPoint { get; set; }
+
+    /// <summary>
+    /// Gets or sets the playable media types.
+    /// </summary>
+    /// <value>The playable media types.</value>
+    public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
+
+    /// <summary>
+    /// Gets or sets the id.
+    /// </summary>
+    /// <value>The id.</value>
+    public string? Id { get; set; }
+
+    /// <summary>
+    /// Gets or sets the user id.
+    /// </summary>
+    /// <value>The user id.</value>
+    public Guid UserId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the username.
+    /// </summary>
+    /// <value>The username.</value>
+    public string? UserName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the type of the client.
+    /// </summary>
+    /// <value>The type of the client.</value>
+    public string? Client { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last activity date.
+    /// </summary>
+    /// <value>The last activity date.</value>
+    public DateTime LastActivityDate { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last playback check in.
+    /// </summary>
+    /// <value>The last playback check in.</value>
+    public DateTime LastPlaybackCheckIn { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last paused date.
+    /// </summary>
+    /// <value>The last paused date.</value>
+    public DateTime? LastPausedDate { get; set; }
+
+    /// <summary>
+    /// Gets or sets the name of the device.
+    /// </summary>
+    /// <value>The name of the device.</value>
+    public string? DeviceName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the type of the device.
+    /// </summary>
+    /// <value>The type of the device.</value>
+    public string? DeviceType { get; set; }
+
+    /// <summary>
+    /// Gets or sets the now playing item.
+    /// </summary>
+    /// <value>The now playing item.</value>
+    public BaseItemDto? NowPlayingItem { get; set; }
+
+    /// <summary>
+    /// Gets or sets the now viewing item.
+    /// </summary>
+    /// <value>The now viewing item.</value>
+    public BaseItemDto? NowViewingItem { get; set; }
+
+    /// <summary>
+    /// Gets or sets the device id.
+    /// </summary>
+    /// <value>The device id.</value>
+    public string? DeviceId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the application version.
+    /// </summary>
+    /// <value>The application version.</value>
+    public string? ApplicationVersion { get; set; }
+
+    /// <summary>
+    /// Gets or sets the transcoding info.
+    /// </summary>
+    /// <value>The transcoding info.</value>
+    public TranscodingInfo? TranscodingInfo { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether this session is active.
+    /// </summary>
+    /// <value><c>true</c> if this session is active; otherwise, <c>false</c>.</value>
+    public bool IsActive { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the session supports media control.
+    /// </summary>
+    /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value>
+    public bool SupportsMediaControl { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the session supports remote control.
+    /// </summary>
+    /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value>
+    public bool SupportsRemoteControl { get; set; }
+
+    /// <summary>
+    /// Gets or sets the now playing queue.
+    /// </summary>
+    /// <value>The now playing queue.</value>
+    public IReadOnlyList<QueueItem>? NowPlayingQueue { get; set; }
+
+    /// <summary>
+    /// Gets or sets the now playing queue full items.
+    /// </summary>
+    /// <value>The now playing queue full items.</value>
+    public IReadOnlyList<BaseItemDto>? NowPlayingQueueFullItems { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the session has a custom device name.
+    /// </summary>
+    /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
+    public bool HasCustomDeviceName { get; set; }
+
+    /// <summary>
+    /// Gets or sets the playlist item id.
+    /// </summary>
+    /// <value>The splaylist item id.</value>
+    public string? PlaylistItemId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the server id.
+    /// </summary>
+    /// <value>The server id.</value>
+    public string? ServerId { get; set; }
+
+    /// <summary>
+    /// Gets or sets the user primary image tag.
+    /// </summary>
+    /// <value>The user primary image tag.</value>
+    public string? UserPrimaryImageTag { get; set; }
+
+    /// <summary>
+    /// Gets or sets the supported commands.
+    /// </summary>
+    /// <value>The supported commands.</value>
+    public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
+}

+ 44 - 21
src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
+using System.Linq;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
@@ -35,38 +37,27 @@ namespace Jellyfin.Extensions.Json.Converters
                 var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
                 if (stringEntries.Length == 0)
                 {
-                    return Array.Empty<T>();
+                    return [];
                 }
 
-                var parsedValues = new object[stringEntries.Length];
-                var convertedCount = 0;
+                var typedValues = new List<T>();
                 for (var i = 0; i < stringEntries.Length; i++)
                 {
                     try
                     {
-                        parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException();
-                        convertedCount++;
+                        var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim());
+                        if (parsedValue is not null)
+                        {
+                            typedValues.Add((T)parsedValue);
+                        }
                     }
                     catch (FormatException)
                     {
-                        // TODO log when upgraded to .Net6
-                        // https://github.com/dotnet/runtime/issues/42975
-                        // _logger.LogDebug(e, "Error converting value.");
+                        // Ignore unconvertable inputs
                     }
                 }
 
-                var typedValues = new T[convertedCount];
-                var typedValueIndex = 0;
-                for (var i = 0; i < stringEntries.Length; i++)
-                {
-                    if (parsedValues[i] is not null)
-                    {
-                        typedValues.SetValue(parsedValues[i], typedValueIndex);
-                        typedValueIndex++;
-                    }
-                }
-
-                return typedValues;
+                return [.. typedValues];
             }
 
             return JsonSerializer.Deserialize<T[]>(ref reader, options);
@@ -75,7 +66,39 @@ namespace Jellyfin.Extensions.Json.Converters
         /// <inheritdoc />
         public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
         {
-            throw new NotImplementedException();
+            if (value is not null)
+            {
+                writer.WriteStartArray();
+                if (value.Length > 0)
+                {
+                    var toWrite = value.Length - 1;
+                    foreach (var it in value)
+                    {
+                        var wrote = false;
+                        if (it is not null)
+                        {
+                            writer.WriteStringValue(it.ToString());
+                            wrote = true;
+                        }
+
+                        if (toWrite > 0)
+                        {
+                            if (wrote)
+                            {
+                                writer.WriteStringValue(Delimiter.ToString());
+                            }
+
+                            toWrite--;
+                        }
+                    }
+                }
+
+                writer.WriteEndArray();
+            }
+            else
+            {
+                writer.WriteNullValue();
+            }
         }
     }
 }

+ 8 - 8
tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs

@@ -41,7 +41,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<string>
             {
-                Value = new[] { "a", "b", "c" }
+                Value = ["a", "b", "c"]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
@@ -53,7 +53,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<string>
             {
-                Value = new[] { "a", "b", "c" }
+                Value = ["a", "b", "c"]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
@@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
             {
-                Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+                Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
@@ -77,7 +77,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
             {
-                Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+                Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions);
@@ -89,7 +89,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
             {
-                Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+                Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions);
@@ -101,7 +101,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
             {
-                Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+                Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
@@ -113,7 +113,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<string>
             {
-                Value = new[] { "a", "b", "c" }
+                Value = ["a", "b", "c"]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
@@ -125,7 +125,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
         {
             var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
             {
-                Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+                Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
             };
 
             var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);