Merge pull request #3194 from OancaAndrei/syncplay-enhanced

SyncPlay for TV series (and Music)
This commit is contained in:
Joshua M. Boniface
2020-12-04 20:22:30 -05:00
committed by GitHub
91 changed files with 5555 additions and 1356 deletions

View File

@@ -4,9 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SyncPlayDtos;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
using MediaBrowser.Controller.SyncPlay.Requests;
using MediaBrowser.Model.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -17,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// The sync play controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
[Authorize(Policy = Policies.SyncPlayAccess)]
public class SyncPlayController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
@@ -43,35 +46,36 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Create a new SyncPlay group.
/// </summary>
/// <param name="requestData">The settings of the new group.</param>
/// <response code="204">New group created.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayCreateGroup()
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
public ActionResult SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
_syncPlayManager.NewGroup(currentSession, CancellationToken.None);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Join an existing SyncPlay group.
/// </summary>
/// <param name="groupId">The sync play group id.</param>
/// <param name="requestData">The group to join.</param>
/// <response code="204">Group join successful.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
[Authorize(Policy = Policies.SyncPlayAccess)]
public ActionResult SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var joinRequest = new JoinGroupRequest()
{
GroupId = groupId
};
_syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -85,38 +89,125 @@ namespace Jellyfin.Api.Controllers
public ActionResult SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
_syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
var syncPlayRequest = new LeaveGroupRequest();
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Gets all SyncPlay groups.
/// </summary>
/// <param name="filterItemId">Optional. Filter by item id.</param>
/// <response code="200">Groups returned.</response>
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
[Authorize(Policy = Policies.SyncPlayAccess)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
var syncPlayRequest = new ListGroupsRequest();
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
}
/// <summary>
/// Request play in SyncPlay group.
/// Request to set new playlist in SyncPlay group.
/// </summary>
/// <response code="204">Play request sent to all group members.</response>
/// <param name="requestData">The new playlist to play in the group.</param>
/// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Play")]
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPlay()
public ActionResult SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PlaybackRequest()
{
Type = PlaybackRequestType.Play
};
var syncPlayRequest = new PlayGroupRequest(
requestData.PlayingQueue,
requestData.PlayingItemPosition,
requestData.StartPositionTicks);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to change playlist item in SyncPlay group.
/// </summary>
/// <param name="requestData">The new item to play.</param>
/// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to remove items from the playlist in SyncPlay group.
/// </summary>
/// <param name="requestData">The items to remove.</param>
/// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to move an item in the playlist in SyncPlay group.
/// </summary>
/// <param name="requestData">The new position for the item.</param>
/// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to queue items to the playlist of a SyncPlay group.
/// </summary>
/// <param name="requestData">The items to add.</param>
/// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request unpause in SyncPlay group.
/// </summary>
/// <response code="204">Unpause update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayUnpause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new UnpauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -124,17 +215,29 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Request pause in SyncPlay group.
/// </summary>
/// <response code="204">Pause request sent to all group members.</response>
/// <response code="204">Pause update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PlaybackRequest()
{
Type = PlaybackRequestType.Pause
};
var syncPlayRequest = new PauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request stop in SyncPlay group.
/// </summary>
/// <response code="204">Stop update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayStop()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new StopGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -142,42 +245,143 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Request seek in SyncPlay group.
/// </summary>
/// <param name="positionTicks">The playback position in ticks.</param>
/// <response code="204">Seek request sent to all group members.</response>
/// <param name="requestData">The new playback position.</param>
/// <response code="204">Seek update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
public ActionResult SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PlaybackRequest()
{
Type = PlaybackRequestType.Seek,
PositionTicks = positionTicks
};
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request group wait in SyncPlay group while buffering.
/// Notify SyncPlay group that member is buffering.
/// </summary>
/// <param name="when">When the request has been made by the client.</param>
/// <param name="positionTicks">The playback position in ticks.</param>
/// <param name="bufferingDone">Whether the buffering is done.</param>
/// <response code="204">Buffering request sent to all group members.</response>
/// <param name="requestData">The player status.</param>
/// <response code="204">Group state update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
public ActionResult SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PlaybackRequest()
{
Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
When = when,
PositionTicks = positionTicks
};
var syncPlayRequest = new BufferGroupRequest(
requestData.When,
requestData.PositionTicks,
requestData.IsPlaying,
requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Notify SyncPlay group that member is ready for playback.
/// </summary>
/// <param name="requestData">The player status.</param>
/// <response code="204">Group state update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new ReadyGroupRequest(
requestData.When,
requestData.PositionTicks,
requestData.IsPlaying,
requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request SyncPlay group to ignore member during group-wait.
/// </summary>
/// <param name="requestData">The settings to set.</param>
/// <response code="204">Member state updated.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request next item in SyncPlay group.
/// </summary>
/// <param name="requestData">The current item information.</param>
/// <response code="204">Next item update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request previous item in SyncPlay group.
/// </summary>
/// <param name="requestData">The current item information.</param>
/// <response code="204">Previous item update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to set repeat mode in SyncPlay group.
/// </summary>
/// <param name="requestData">The new repeat mode.</param>
/// <response code="204">Play queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Request to set shuffle mode in SyncPlay group.
/// </summary>
/// <param name="requestData">The new shuffle mode.</param>
/// <response code="204">Play queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -185,19 +389,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Update session ping.
/// </summary>
/// <param name="ping">The ping.</param>
/// <param name="requestData">The new ping.</param>
/// <response code="204">Ping updated.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPing([FromQuery] double ping)
public ActionResult SyncPlayPing(
[FromBody, Required] PingRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
var syncPlayRequest = new PlaybackRequest()
{
Type = PlaybackRequestType.Ping,
Ping = Convert.ToInt64(ping)
};
var syncPlayRequest = new PingGroupRequest(requestData.Ping);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}

View File

@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Controllers
public class TimeSyncController : BaseJellyfinApiController
{
/// <summary>
/// Gets the current utc time.
/// Gets the current UTC time.
/// </summary>
/// <response code="200">Time returned.</response>
/// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
@@ -22,18 +22,14 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UtcTimeResponse> GetUtcTime()
{
// Important to keep the following line at the beginning
var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
var response = new UtcTimeResponse();
response.RequestReceptionTime = requestReceptionTime;
// Important to keep the following two lines at the end
var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
response.ResponseTransmissionTime = responseTransmissionTime;
// Important to keep the following line at the end
var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
return response;
return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
}
}
}