#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 { /// /// The session controller. /// public class SessionController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; private readonly IDeviceManager _deviceManager; private readonly ISessionContext _sessionContext; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. /// Instance of interface. /// Instance of interface. /// Instance of interface. public SessionController( ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, IDeviceManager deviceManager, ISessionContext sessionContext) { _sessionManager = sessionManager; _userManager = userManager; _authContext = authContext; _deviceManager = deviceManager; _sessionContext = sessionContext; } /// /// Gets a list of sessions. /// /// Filter by sessions that a given user is allowed to remote control. /// Filter by device Id. /// Optional. Filter by sessions that were active in the last n seconds. /// List of sessions returned. /// An with the available sessions. [HttpGet("/Sessions")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> 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); } /// /// Instructs a session to browse to an item or view. /// /// The session Id. /// The type of item to browse to. /// The Id of the item. /// The name of the item. /// Instruction sent to session. /// A . [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(); } /// /// Instructs a session to play an item. /// /// The session id. /// The ids of the items to play, comma delimited. /// The starting position of the first item. /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. /// The . /// Instruction sent to session. /// A . [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(); } /// /// Issues a playstate command to a client. /// /// The session id. /// The . /// Playstate command sent to session. /// A . [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(); } /// /// Issues a system command to a client. /// /// The session id. /// The command to send. /// System command sent to session. /// A . [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(); } /// /// Issues a general command to a client. /// /// The session id. /// The command to send. /// General command sent to session. /// A . [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(); } /// /// Issues a full general command to a client. /// /// The session id. /// The . /// Full general command sent to session. /// A . [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(); } /// /// Issues a command to a client to display a message to the user. /// /// The session id. /// The message test. /// The message header. /// The message timeout. If omitted the user will have to confirm viewing the message. /// Message sent. /// A . [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(); } /// /// Adds an additional user to a session. /// /// The session id. /// The user id. /// User added to session. /// A . [HttpPost("/Sessions/{id}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( [FromRoute] string id, [FromRoute] Guid userId) { _sessionManager.AddAdditionalUser(id, userId); return NoContent(); } /// /// Removes an additional user from a session. /// /// The session id. /// The user id. /// User removed from session. /// A . [HttpDelete("/Sessions/{id}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute] string id, [FromRoute] Guid userId) { _sessionManager.RemoveAdditionalUser(id, userId); return NoContent(); } /// /// Updates capabilities for a device. /// /// The session id. /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. /// A list of supported remote control commands, comma delimited. /// Determines whether media can be played remotely.. /// Determines whether sync is supported. /// Determines whether the device supports a unique identifier. /// Capabilities posted. /// A . [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(); } /// /// Updates capabilities for a device. /// /// The session id. /// The . /// Capabilities updated. /// A . [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(); } /// /// Reports that a session is viewing an item. /// /// The session id. /// The item id. /// Session reported to server. /// A . [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(); } /// /// Reports that a session has ended. /// /// Session end reported to server. /// A . [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(); } /// /// Get all auth providers. /// /// Auth providers retrieved. /// An with the auth providers. [HttpGet("/Auth/Providers")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAuthProviders() { return _userManager.GetAuthenticationProviders(); } /// /// Get all password reset providers. /// /// Password reset providers retrieved. /// An with the password reset providers. [HttpGet("/Auto/PasswordResetProviders")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPasswordResetProviders() { return _userManager.GetPasswordResetProviders(); } } }