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;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers;
///
/// The session controller.
///
[Route("")]
public class SessionController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
private readonly IDeviceManager _deviceManager;
///
/// Initializes a new instance of the class.
///
/// Instance of interface.
/// Instance of interface.
/// Instance of interface.
public SessionController(
ISessionManager sessionManager,
IUserManager userManager,
IDeviceManager deviceManager)
{
_sessionManager = sessionManager;
_userManager = userManager;
_deviceManager = deviceManager;
}
///
/// 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.IsNullOrEmpty())
{
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 (!User.IsInRole(UserRoles.Administrator))
{
// 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);
}
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/{sessionId}/Viewing")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task DisplayContent(
[FromRoute, Required] string sessionId,
[FromQuery, Required] BaseItemKind itemType,
[FromQuery, Required] string itemId,
[FromQuery, Required] string itemName)
{
var command = new BrowseRequest
{
ItemId = itemId,
ItemName = itemName,
ItemType = itemType
};
await _sessionManager.SendBrowseCommand(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
command,
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Instructs a session to play an item.
///
/// The session id.
/// 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 ids of the items to play, comma delimited.
/// The starting position of the first item.
/// Optional. The media source id.
/// Optional. The index of the audio stream to play.
/// Optional. The index of the subtitle stream to play.
/// Optional. The start index.
/// Instruction sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/Playing")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? startIndex)
{
var playRequest = new PlayRequest
{
ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand,
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
StartIndex = startIndex
};
await _sessionManager.SendPlayCommand(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
playRequest,
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Issues a playstate command to a client.
///
/// The session id.
/// The .
/// The optional position ticks.
/// The optional controlling user id.
/// Playstate command sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task SendPlaystateCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] PlaystateCommand command,
[FromQuery] long? seekPositionTicks,
[FromQuery] string? controllingUserId)
{
await _sessionManager.SendPlaystateCommand(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
new PlaystateRequest()
{
Command = command,
ControllingUserId = controllingUserId,
SeekPositionTicks = seekPositionTicks,
},
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Issues a system command to a client.
///
/// The session id.
/// The command to send.
/// System command sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/System/{command}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task SendSystemCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
Name = command,
ControllingUserId = currentSession.UserId
};
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
///
/// Issues a general command to a client.
///
/// The session id.
/// The command to send.
/// General command sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/Command/{command}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task SendGeneralCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
Name = command,
ControllingUserId = currentSession.UserId
};
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Issues a full general command to a client.
///
/// The session id.
/// The .
/// Full general command sent to session.
/// A .
[HttpPost("Sessions/{sessionId}/Command")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task SendFullGeneralCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] GeneralCommand command)
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(command);
command.ControllingUserId = currentSession.UserId;
await _sessionManager.SendGeneralCommand(
currentSession.Id,
sessionId,
command,
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Issues a command to a client to display a message to the user.
///
/// The session id.
/// The object containing Header, Message Text, and TimeoutMs.
/// Message sent.
/// A .
[HttpPost("Sessions/{sessionId}/Message")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task SendMessageCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] MessageCommand command)
{
if (string.IsNullOrWhiteSpace(command.Header))
{
command.Header = "Message from Server";
}
await _sessionManager.SendMessageCommand(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
command,
CancellationToken.None)
.ConfigureAwait(false);
return NoContent();
}
///
/// Adds an additional user to a session.
///
/// The session id.
/// The user id.
/// User added to session.
/// A .
[HttpPost("Sessions/{sessionId}/User/{userId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
[FromRoute, Required] string sessionId,
[FromRoute, Required] Guid userId)
{
_sessionManager.AddAdditionalUser(sessionId, userId);
return NoContent();
}
///
/// Removes an additional user from a session.
///
/// The session id.
/// The user id.
/// User removed from session.
/// A .
[HttpDelete("Sessions/{sessionId}/User/{userId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
[FromRoute, Required] string sessionId,
[FromRoute, Required] Guid userId)
{
_sessionManager.RemoveAdditionalUser(sessionId, 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 the device supports a unique identifier.
/// Capabilities posted.
/// A .
[HttpPost("Sessions/Capabilities")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task PostCapabilities(
[FromQuery] string? id,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsPersistentIdentifier = true)
{
if (string.IsNullOrWhiteSpace(id))
{
id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsPersistentIdentifier = supportsPersistentIdentifier
});
return NoContent();
}
///
/// Updates capabilities for a device.
///
/// The session id.
/// The .
/// Capabilities updated.
/// A .
[HttpPost("Sessions/Capabilities/Full")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task PostFullCapabilities(
[FromQuery] string? id,
[FromBody, Required] ClientCapabilitiesDto capabilities)
{
if (string.IsNullOrWhiteSpace(id))
{
id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
return NoContent();
}
///
/// Reports that a session is viewing an item.
///
/// The session id.
/// The item id.
/// Session reported to server.
/// A .
[HttpPost("Sessions/Viewing")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task ReportViewing(
[FromQuery] string? sessionId,
[FromQuery, Required] string? itemId)
{
string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
}
///
/// Reports that a session has ended.
///
/// Session end reported to server.
/// A .
[HttpPost("Sessions/Logout")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task ReportSessionEnded()
{
await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false);
return NoContent();
}
///
/// Get all auth providers.
///
/// Auth providers retrieved.
/// An with the auth providers.
[HttpGet("Auth/Providers")]
[Authorize(Policy = Policies.RequiresElevation)]
[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("Auth/PasswordResetProviders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult> GetPasswordResetProviders()
{
return _userManager.GetPasswordResetProviders();
}
}