2020-04-01 08:52:42 -07:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
2020-05-26 17:52:05 -07:00
using Jellyfin.Data.Enums ;
2020-04-01 08:52:42 -07:00
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
2020-05-06 14:42:53 -07:00
using MediaBrowser.Controller.SyncPlay ;
using MediaBrowser.Model.SyncPlay ;
2020-05-26 17:52:05 -07:00
using Microsoft.Extensions.Logging ;
2020-04-01 08:52:42 -07:00
2020-05-06 14:42:53 -07:00
namespace Emby.Server.Implementations.SyncPlay
2020-04-01 08:52:42 -07:00
{
/// <summary>
2020-05-06 14:42:53 -07:00
/// Class SyncPlayManager.
2020-04-01 08:52:42 -07:00
/// </summary>
2020-05-06 14:42:53 -07:00
public class SyncPlayManager : ISyncPlayManager , IDisposable
2020-04-01 08:52:42 -07:00
{
/// <summary>
/// The logger.
/// </summary>
2020-06-05 17:15:56 -07:00
private readonly ILogger < SyncPlayManager > _logger ;
2020-04-01 08:52:42 -07:00
2020-04-04 08:59:16 -07:00
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager ;
2020-04-01 08:52:42 -07:00
/// <summary>
/// The session manager.
/// </summary>
private readonly ISessionManager _sessionManager ;
2020-04-04 15:50:57 -07:00
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager ;
2020-04-01 08:52:42 -07:00
/// <summary>
2020-04-04 08:56:21 -07:00
/// The map between sessions and groups.
2020-04-01 08:52:42 -07:00
/// </summary>
2020-11-13 07:13:32 -07:00
private readonly Dictionary < string , IGroupController > _sessionToGroupMap =
new Dictionary < string , IGroupController > ( StringComparer . OrdinalIgnoreCase ) ;
2020-04-01 08:52:42 -07:00
/// <summary>
/// The groups.
/// </summary>
2020-11-13 07:13:32 -07:00
private readonly Dictionary < Guid , IGroupController > _groups =
new Dictionary < Guid , IGroupController > ( ) ;
2020-04-28 05:12:06 -07:00
/// <summary>
/// Lock used for accesing any group.
/// </summary>
private readonly object _groupsLock = new object ( ) ;
2020-04-01 08:52:42 -07:00
private bool _disposed = false ;
2020-05-29 02:28:19 -07:00
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="libraryManager">The library manager.</param>
2020-05-06 14:42:53 -07:00
public SyncPlayManager (
ILogger < SyncPlayManager > logger ,
2020-04-04 08:59:16 -07:00
IUserManager userManager ,
2020-04-04 15:50:57 -07:00
ISessionManager sessionManager ,
ILibraryManager libraryManager )
2020-04-01 08:52:42 -07:00
{
_logger = logger ;
2020-04-04 08:59:16 -07:00
_userManager = userManager ;
2020-04-01 08:52:42 -07:00
_sessionManager = sessionManager ;
2020-04-04 15:50:57 -07:00
_libraryManager = libraryManager ;
2020-09-24 14:04:21 -07:00
_sessionManager . SessionStarted + = OnSessionManagerSessionStarted ;
2020-04-01 08:52:42 -07:00
}
/// <inheritdoc />
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
/// <inheritdoc />
2020-09-24 14:04:21 -07:00
public void NewGroup ( SessionInfo session , NewGroupRequest request , CancellationToken cancellationToken )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . NewGroup , request ) )
2020-04-01 08:52:42 -07:00
{
2020-04-22 13:05:53 -07:00
return ;
2020-04-04 08:59:16 -07:00
}
2020-04-28 05:12:06 -07:00
lock ( _groupsLock )
2020-04-04 08:59:16 -07:00
{
2020-04-28 05:12:06 -07:00
if ( IsSessionInGroup ( session ) )
{
2020-05-04 10:46:02 -07:00
LeaveGroup ( session , cancellationToken ) ;
2020-04-28 05:12:06 -07:00
}
2020-04-01 08:52:42 -07:00
2020-11-15 09:03:27 -07:00
var group = new GroupController ( _logger , _userManager , _sessionManager , _libraryManager ) ;
2020-09-24 14:04:21 -07:00
_groups [ group . GroupId ] = group ;
2020-04-01 08:52:42 -07:00
2020-11-15 09:03:27 -07:00
AddSessionToGroup ( session , group ) ;
2020-09-24 14:04:21 -07:00
group . CreateGroup ( session , request , cancellationToken ) ;
2020-04-28 05:12:06 -07:00
}
2020-04-01 08:52:42 -07:00
}
/// <inheritdoc />
2020-05-09 05:34:07 -07:00
public void JoinGroup ( SessionInfo session , Guid groupId , JoinGroupRequest request , CancellationToken cancellationToken )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . JoinGroup , request ) )
2020-04-04 08:59:16 -07:00
{
2020-04-22 13:05:53 -07:00
return ;
2020-04-04 08:59:16 -07:00
}
2020-10-22 06:40:34 -07:00
var user = _userManager . GetUserById ( session . UserId ) ;
2020-04-28 05:12:06 -07:00
lock ( _groupsLock )
2020-04-01 08:52:42 -07:00
{
2020-11-13 07:13:32 -07:00
_groups . TryGetValue ( groupId , out IGroupController group ) ;
2020-04-01 08:52:42 -07:00
2020-04-28 05:12:06 -07:00
if ( group = = null )
2020-04-22 13:05:53 -07:00
{
2020-11-15 09:03:27 -07:00
_logger . LogWarning ( "JoinGroup: {SessionId} tried to join group {GroupId} that does not exist." , session . Id , groupId ) ;
2020-04-04 15:50:57 -07:00
2020-11-15 09:03:27 -07:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . GroupDoesNotExist , string . Empty ) ;
2020-09-24 14:04:21 -07:00
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
2020-04-28 05:12:06 -07:00
return ;
}
2020-04-22 13:05:53 -07:00
2020-09-24 14:04:21 -07:00
if ( ! group . HasAccessToPlayQueue ( user ) )
2020-04-22 13:05:53 -07:00
{
2020-11-15 09:03:27 -07:00
_logger . LogWarning ( "JoinGroup: {SessionId} does not have access to some content from the playing queue of group {GroupId}." , session . Id , group . GroupId . ToString ( ) ) ;
2020-04-28 05:12:06 -07:00
2020-11-15 09:03:27 -07:00
var error = new GroupUpdate < string > ( group . GroupId , GroupUpdateType . LibraryAccessDenied , string . Empty ) ;
2020-09-24 14:04:21 -07:00
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
2020-04-28 05:12:06 -07:00
return ;
}
if ( IsSessionInGroup ( session ) )
{
2020-05-09 05:34:07 -07:00
if ( GetSessionGroup ( session ) . Equals ( groupId ) )
{
2020-09-24 14:04:21 -07:00
group . SessionRestore ( session , request , cancellationToken ) ;
2020-05-09 05:34:07 -07:00
return ;
}
2020-05-04 10:46:02 -07:00
LeaveGroup ( session , cancellationToken ) ;
2020-04-28 05:12:06 -07:00
}
2020-04-04 15:50:57 -07:00
2020-11-15 09:03:27 -07:00
AddSessionToGroup ( session , group ) ;
2020-05-04 10:46:02 -07:00
group . SessionJoin ( session , request , cancellationToken ) ;
2020-04-21 14:37:37 -07:00
}
2020-04-01 08:52:42 -07:00
}
/// <inheritdoc />
2020-05-04 10:46:02 -07:00
public void LeaveGroup ( SessionInfo session , CancellationToken cancellationToken )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . LeaveGroup ) )
{
return ;
}
2020-10-21 06:46:50 -07:00
// TODO: determine what happens to users that are in a group and get their permissions revoked.
2020-04-28 05:12:06 -07:00
lock ( _groupsLock )
2020-04-01 08:52:42 -07:00
{
2020-05-28 11:21:26 -07:00
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
2020-04-01 08:52:42 -07:00
2020-04-28 05:12:06 -07:00
if ( group = = null )
2020-04-22 13:05:53 -07:00
{
2020-11-15 09:03:27 -07:00
_logger . LogWarning ( "LeaveGroup: {SessionId} does not belong to any group." , session . Id ) ;
2020-04-01 08:52:42 -07:00
2020-11-15 09:03:27 -07:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . NotInGroup , string . Empty ) ;
2020-09-24 14:04:21 -07:00
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
2020-04-28 05:12:06 -07:00
return ;
}
2020-11-15 09:03:27 -07:00
RemoveSessionFromGroup ( session , group ) ;
2020-05-04 10:46:02 -07:00
group . SessionLeave ( session , cancellationToken ) ;
2020-04-28 05:12:06 -07:00
if ( group . IsGroupEmpty ( ) )
{
2020-11-15 09:03:27 -07:00
_logger . LogInformation ( "LeaveGroup: removing empty group {GroupId}." , group . GroupId ) ;
2020-09-24 14:04:21 -07:00
_groups . Remove ( group . GroupId , out _ ) ;
2020-04-28 05:12:06 -07:00
}
2020-04-01 08:52:42 -07:00
}
}
/// <inheritdoc />
2020-09-24 14:04:21 -07:00
public List < GroupInfoDto > ListGroups ( SessionInfo session )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . ListGroups ) )
2020-04-04 08:59:16 -07:00
{
2020-05-12 00:08:35 -07:00
return new List < GroupInfoDto > ( ) ;
2020-04-04 08:59:16 -07:00
}
2020-10-22 06:40:34 -07:00
var user = _userManager . GetUserById ( session . UserId ) ;
2020-11-15 09:03:27 -07:00
lock ( _groupsLock )
{
return _groups
. Values
. Where ( group = > group . HasAccessToPlayQueue ( user ) )
. Select ( group = > group . GetInfo ( ) )
. ToList ( ) ;
}
2020-04-01 08:52:42 -07:00
}
/// <inheritdoc />
2020-11-13 07:13:32 -07:00
public void HandleRequest ( SessionInfo session , IGroupPlaybackRequest request , CancellationToken cancellationToken )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . Playback , request ) )
2020-04-04 08:59:16 -07:00
{
2020-04-22 13:05:53 -07:00
return ;
2020-04-04 08:59:16 -07:00
}
2020-04-28 05:12:06 -07:00
lock ( _groupsLock )
2020-04-01 08:52:42 -07:00
{
2020-05-28 11:21:26 -07:00
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
2020-04-01 08:52:42 -07:00
2020-04-28 05:12:06 -07:00
if ( group = = null )
2020-04-22 13:05:53 -07:00
{
2020-11-15 09:03:27 -07:00
_logger . LogWarning ( "HandleRequest: {SessionId} does not belong to any group." , session . Id ) ;
2020-04-28 05:12:06 -07:00
2020-11-15 09:03:27 -07:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . NotInGroup , string . Empty ) ;
2020-09-24 14:04:21 -07:00
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
2020-04-28 05:12:06 -07:00
return ;
}
2020-05-04 10:46:02 -07:00
group . HandleRequest ( session , request , cancellationToken ) ;
2020-04-01 08:52:42 -07:00
}
}
2020-04-15 09:03:58 -07:00
2020-11-15 09:03:27 -07:00
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose ( bool disposing )
{
if ( _disposed )
{
return ;
}
_sessionManager . SessionStarted - = OnSessionManagerSessionStarted ;
_disposed = true ;
}
private void OnSessionManagerSessionStarted ( object sender , SessionEventArgs e )
{
var session = e . SessionInfo ;
lock ( _groupsLock )
{
if ( ! IsSessionInGroup ( session ) )
{
return ;
}
var groupId = GetSessionGroup ( session ) ;
var request = new JoinGroupRequest ( groupId ) ;
JoinGroup ( session , groupId , request , CancellationToken . None ) ;
}
}
/// <summary>
/// Checks if a given session has joined a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <returns><c>true</c> if the session has joined a group, <c>false</c> otherwise.</returns>
private bool IsSessionInGroup ( SessionInfo session )
{
return _sessionToGroupMap . ContainsKey ( session . Id ) ;
}
/// <summary>
/// Gets the group joined by the given session, if any.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <returns>The group identifier if the session has joined a group, an empty identifier otherwise.</returns>
private Guid GetSessionGroup ( SessionInfo session )
{
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
return group ? . GroupId ? ? Guid . Empty ;
}
/// <summary>
/// Maps a session to a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <param name="group">The group.</param>
/// <exception cref="InvalidOperationException">Thrown when the user is in another group already.</exception>
private void AddSessionToGroup ( SessionInfo session , IGroupController group )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
if ( session = = null )
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2020-04-04 08:56:21 -07:00
if ( IsSessionInGroup ( session ) )
2020-04-01 08:52:42 -07:00
{
2020-04-04 08:56:21 -07:00
throw new InvalidOperationException ( "Session in other group already!" ) ;
2020-04-01 08:52:42 -07:00
}
2020-05-26 02:37:52 -07:00
2020-11-13 07:13:32 -07:00
_sessionToGroupMap [ session . Id ] = group ? ? throw new InvalidOperationException ( "Group is null!" ) ;
2020-04-01 08:52:42 -07:00
}
2020-11-15 09:03:27 -07:00
/// <summary>
/// Unmaps a session from a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <param name="group">The group.</param>
/// <exception cref="InvalidOperationException">Thrown when the user is not found in the specified group.</exception>
private void RemoveSessionFromGroup ( SessionInfo session , IGroupController group )
2020-04-01 08:52:42 -07:00
{
2020-10-22 06:40:34 -07:00
if ( session = = null )
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
if ( group = = null )
{
throw new InvalidOperationException ( "Group is null!" ) ;
}
2020-04-04 08:56:21 -07:00
if ( ! IsSessionInGroup ( session ) )
2020-04-01 08:52:42 -07:00
{
2020-04-04 08:56:21 -07:00
throw new InvalidOperationException ( "Session not in any group!" ) ;
2020-04-01 08:52:42 -07:00
}
2020-05-28 11:21:26 -07:00
_sessionToGroupMap . Remove ( session . Id , out var tempGroup ) ;
2020-09-24 14:04:21 -07:00
if ( ! tempGroup . GroupId . Equals ( group . GroupId ) )
2020-04-01 08:52:42 -07:00
{
2020-04-04 08:56:21 -07:00
throw new InvalidOperationException ( "Session was in wrong group!" ) ;
2020-04-01 08:52:42 -07:00
}
}
2020-11-15 09:03:27 -07:00
/// <summary>
/// Checks if a given session is allowed to make a given request.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="requestType">The request type.</param>
/// <param name="request">The request.</param>
/// <param name="checkRequest">Whether to check if request is null.</param>
/// <returns><c>true</c> if the request is valid, <c>false</c> otherwise. Will return <c>false</c> also when session is null.</returns>
private bool IsRequestValid < T > ( SessionInfo session , GroupRequestType requestType , T request , bool checkRequest = true )
{
if ( session = = null | | ( request = = null & & checkRequest ) )
{
return false ;
}
var user = _userManager . GetUserById ( session . UserId ) ;
if ( user . SyncPlayAccess = = SyncPlayAccess . None )
{
_logger . LogWarning ( "IsRequestValid: {SessionId} does not have access to SyncPlay. Requested {RequestType}." , session . Id , requestType ) ;
// TODO: rename to a more generic error. Next PR will fix this.
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . JoinGroupDenied , string . Empty ) ;
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
return false ;
}
if ( requestType . Equals ( GroupRequestType . NewGroup ) & & user . SyncPlayAccess ! = SyncPlayAccess . CreateAndJoinGroups )
{
_logger . LogWarning ( "IsRequestValid: {SessionId} does not have permission to create groups." , session . Id ) ;
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . CreateGroupDenied , string . Empty ) ;
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
return false ;
}
return true ;
}
/// <summary>
/// Checks if a given session is allowed to make a given type of request.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="requestType">The request type.</param>
/// <returns><c>true</c> if the request is valid, <c>false</c> otherwise. Will return <c>false</c> also when session is null.</returns>
private bool IsRequestValid ( SessionInfo session , GroupRequestType requestType )
{
return IsRequestValid ( session , requestType , session , false ) ;
}
2020-04-01 08:52:42 -07:00
}
}