2020-04-01 15:52:42 +00:00
using System ;
using System.Collections.Generic ;
using System.Threading ;
2020-05-27 00:52:05 +00:00
using Jellyfin.Data.Enums ;
2020-04-01 15:52:42 +00:00
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
2020-05-06 21:42:53 +00:00
using MediaBrowser.Controller.SyncPlay ;
using MediaBrowser.Model.SyncPlay ;
2020-11-28 13:19:24 +00:00
using MediaBrowser.Model.SyncPlay.RequestBodies ;
2020-05-27 00:52:05 +00:00
using Microsoft.Extensions.Logging ;
2020-04-01 15:52:42 +00:00
2020-05-06 21:42:53 +00:00
namespace Emby.Server.Implementations.SyncPlay
2020-04-01 15:52:42 +00:00
{
/// <summary>
2020-05-06 21:42:53 +00:00
/// Class SyncPlayManager.
2020-04-01 15:52:42 +00:00
/// </summary>
2020-05-06 21:42:53 +00:00
public class SyncPlayManager : ISyncPlayManager , IDisposable
2020-04-01 15:52:42 +00:00
{
/// <summary>
/// The logger.
/// </summary>
2020-06-06 00:15:56 +00:00
private readonly ILogger < SyncPlayManager > _logger ;
2020-04-01 15:52:42 +00:00
2020-11-16 16:40:19 +00:00
/// <summary>
/// The logger factory.
/// </summary>
private readonly ILoggerFactory _loggerFactory ;
2020-04-04 15:59:16 +00:00
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager ;
2020-04-01 15:52:42 +00:00
/// <summary>
/// The session manager.
/// </summary>
private readonly ISessionManager _sessionManager ;
2020-04-04 22:50:57 +00:00
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager ;
2020-04-01 15:52:42 +00:00
/// <summary>
2020-04-04 15:56:21 +00:00
/// The map between sessions and groups.
2020-04-01 15:52:42 +00:00
/// </summary>
2020-11-13 14:13:32 +00:00
private readonly Dictionary < string , IGroupController > _sessionToGroupMap =
new Dictionary < string , IGroupController > ( StringComparer . OrdinalIgnoreCase ) ;
2020-04-01 15:52:42 +00:00
/// <summary>
/// The groups.
/// </summary>
2020-11-13 14:13:32 +00:00
private readonly Dictionary < Guid , IGroupController > _groups =
new Dictionary < Guid , IGroupController > ( ) ;
2020-04-28 12:12:06 +00:00
/// <summary>
2020-11-18 13:23:45 +00:00
/// Lock used for accessing any group.
2020-04-28 12:12:06 +00:00
/// </summary>
private readonly object _groupsLock = new object ( ) ;
2020-04-01 15:52:42 +00:00
2020-11-16 19:25:13 +00:00
/// <summary>
2020-11-18 12:28:35 +00:00
/// Lock used for accessing the session-to-group map.
2020-11-16 19:25:13 +00:00
/// </summary>
private readonly object _mapsLock = new object ( ) ;
2020-04-01 15:52:42 +00:00
private bool _disposed = false ;
2020-05-29 09:28:19 +00:00
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
/// </summary>
2020-11-16 16:40:19 +00:00
/// <param name="loggerFactory">The logger factory.</param>
2020-05-29 09:28:19 +00:00
/// <param name="userManager">The user manager.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="libraryManager">The library manager.</param>
2020-05-06 21:42:53 +00:00
public SyncPlayManager (
2020-11-16 16:40:19 +00:00
ILoggerFactory loggerFactory ,
2020-04-04 15:59:16 +00:00
IUserManager userManager ,
2020-04-04 22:50:57 +00:00
ISessionManager sessionManager ,
ILibraryManager libraryManager )
2020-04-01 15:52:42 +00:00
{
2020-11-16 16:40:19 +00:00
_loggerFactory = loggerFactory ;
2020-04-04 15:59:16 +00:00
_userManager = userManager ;
2020-04-01 15:52:42 +00:00
_sessionManager = sessionManager ;
2020-04-04 22:50:57 +00:00
_libraryManager = libraryManager ;
2020-11-16 16:40:19 +00:00
_logger = loggerFactory . CreateLogger < SyncPlayManager > ( ) ;
2020-09-24 21:04:21 +00:00
_sessionManager . SessionStarted + = OnSessionManagerSessionStarted ;
2020-04-01 15:52:42 +00:00
}
/// <inheritdoc />
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
/// <inheritdoc />
2020-11-28 13:19:24 +00:00
public void NewGroup ( SessionInfo session , NewGroupRequestBody request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . NewGroup , request ) )
2020-04-01 15:52:42 +00:00
{
2020-04-22 20:05:53 +00:00
return ;
2020-04-04 15:59:16 +00:00
}
2020-11-16 19:25:13 +00:00
// Locking required to access list of groups.
2020-04-28 12:12:06 +00:00
lock ( _groupsLock )
2020-04-04 15:59:16 +00:00
{
2020-11-16 19:25:13 +00:00
// Locking required as session-to-group map will be edited.
// Locking the group is not required as it is not visible yet.
lock ( _mapsLock )
2020-04-28 12:12:06 +00:00
{
2020-11-16 19:25:13 +00:00
if ( IsSessionInGroup ( session ) )
{
LeaveGroup ( session , cancellationToken ) ;
}
2020-04-01 15:52:42 +00:00
2020-11-16 19:25:13 +00:00
var group = new GroupController ( _loggerFactory , _userManager , _sessionManager , _libraryManager ) ;
_groups [ group . GroupId ] = group ;
2020-04-01 15:52:42 +00:00
2020-11-16 19:25:13 +00:00
AddSessionToGroup ( session , group ) ;
group . CreateGroup ( session , request , cancellationToken ) ;
}
2020-04-28 12:12:06 +00:00
}
2020-04-01 15:52:42 +00:00
}
/// <inheritdoc />
2020-11-28 13:19:24 +00:00
public void JoinGroup ( SessionInfo session , Guid groupId , JoinGroupRequestBody request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . JoinGroup , request ) )
2020-04-04 15:59:16 +00:00
{
2020-04-22 20:05:53 +00:00
return ;
2020-04-04 15:59:16 +00:00
}
2020-10-22 13:40:34 +00:00
var user = _userManager . GetUserById ( session . UserId ) ;
2020-11-16 19:25:13 +00:00
// Locking required to access list of groups.
2020-04-28 12:12:06 +00:00
lock ( _groupsLock )
2020-04-01 15:52:42 +00:00
{
2020-11-13 14:13:32 +00:00
_groups . TryGetValue ( groupId , out IGroupController group ) ;
2020-04-01 15:52:42 +00:00
2020-04-28 12:12:06 +00:00
if ( group = = null )
2020-04-22 20:05:53 +00:00
{
2020-11-16 16:40:19 +00:00
_logger . LogWarning ( "Session {SessionId} tried to join group {GroupId} that does not exist." , session . Id , groupId ) ;
2020-04-04 22:50:57 +00:00
2020-11-15 16:03:27 +00:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . GroupDoesNotExist , string . Empty ) ;
2020-09-24 21:04:21 +00:00
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
2020-04-28 12:12:06 +00:00
return ;
}
2020-04-22 20:05:53 +00:00
2020-11-16 19:25:13 +00:00
// Locking required as session-to-group map will be edited.
lock ( _mapsLock )
2020-04-28 12:12:06 +00:00
{
2020-11-16 19:25:13 +00:00
// Group lock required to let other requests end first.
lock ( group )
2020-05-09 12:34:07 +00:00
{
2020-11-16 19:25:13 +00:00
if ( ! group . HasAccessToPlayQueue ( user ) )
{
_logger . LogWarning ( "Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue." , session . Id , group . GroupId . ToString ( ) ) ;
var error = new GroupUpdate < string > ( group . GroupId , GroupUpdateType . LibraryAccessDenied , string . Empty ) ;
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
return ;
}
if ( IsSessionInGroup ( session ) )
{
if ( FindJoinedGroupId ( session ) . Equals ( groupId ) )
{
group . SessionRestore ( session , request , cancellationToken ) ;
return ;
}
LeaveGroup ( session , cancellationToken ) ;
}
AddSessionToGroup ( session , group ) ;
group . SessionJoin ( session , request , cancellationToken ) ;
2020-05-09 12:34:07 +00:00
}
2020-04-28 12:12:06 +00:00
}
2020-04-21 21:37:37 +00:00
}
2020-04-01 15:52:42 +00:00
}
/// <inheritdoc />
2020-05-04 17:46:02 +00:00
public void LeaveGroup ( SessionInfo session , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . LeaveGroup ) )
{
return ;
}
2020-11-16 19:25:13 +00:00
// Locking required to access list of groups.
2020-04-28 12:12:06 +00:00
lock ( _groupsLock )
2020-04-01 15:52:42 +00:00
{
2020-11-16 19:25:13 +00:00
// Locking required as session-to-group map will be edited.
lock ( _mapsLock )
2020-04-22 20:05:53 +00:00
{
2020-11-16 19:25:13 +00:00
var group = FindJoinedGroup ( session ) ;
if ( group = = null )
{
_logger . LogWarning ( "Session {SessionId} does not belong to any group." , session . Id ) ;
2020-04-28 12:12:06 +00:00
2020-11-16 19:25:13 +00:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . NotInGroup , string . Empty ) ;
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
return ;
}
2020-04-28 12:12:06 +00:00
2020-11-16 19:25:13 +00:00
// Group lock required to let other requests end first.
lock ( group )
{
RemoveSessionFromGroup ( session , group ) ;
group . SessionLeave ( session , cancellationToken ) ;
if ( group . IsGroupEmpty ( ) )
{
_logger . LogInformation ( "Group {GroupId} is empty, removing it." , group . GroupId ) ;
_groups . Remove ( group . GroupId , out _ ) ;
}
}
2020-04-28 12:12:06 +00:00
}
2020-04-01 15:52:42 +00:00
}
}
/// <inheritdoc />
2020-09-24 21:04:21 +00:00
public List < GroupInfoDto > ListGroups ( SessionInfo session )
2020-04-01 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . ListGroups ) )
2020-04-04 15:59:16 +00:00
{
2020-05-12 07:08:35 +00:00
return new List < GroupInfoDto > ( ) ;
2020-04-04 15:59:16 +00:00
}
2020-10-22 13:40:34 +00:00
var user = _userManager . GetUserById ( session . UserId ) ;
2020-11-16 19:25:13 +00:00
List < GroupInfoDto > list = new List < GroupInfoDto > ( ) ;
2020-10-22 13:40:34 +00:00
2020-11-16 19:25:13 +00:00
// Locking required to access list of groups.
2020-11-15 16:03:27 +00:00
lock ( _groupsLock )
{
2020-11-16 19:25:13 +00:00
foreach ( var group in _groups . Values )
{
// Locking required as group is not thread-safe.
lock ( group )
{
if ( group . HasAccessToPlayQueue ( user ) )
{
list . Add ( group . GetInfo ( ) ) ;
}
}
}
2020-11-15 16:03:27 +00:00
}
2020-11-16 19:25:13 +00:00
return list ;
2020-04-01 15:52:42 +00:00
}
/// <inheritdoc />
2020-11-13 14:13:32 +00:00
public void HandleRequest ( SessionInfo session , IGroupPlaybackRequest request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
// TODO: create abstract class for GroupRequests to avoid explicit request type here.
if ( ! IsRequestValid ( session , GroupRequestType . Playback , request ) )
2020-04-04 15:59:16 +00:00
{
2020-04-22 20:05:53 +00:00
return ;
2020-04-04 15:59:16 +00:00
}
2020-11-16 19:25:13 +00:00
var group = FindJoinedGroup ( session ) ;
if ( group = = null )
2020-04-01 15:52:42 +00:00
{
2020-11-16 19:25:13 +00:00
_logger . LogWarning ( "Session {SessionId} does not belong to any group." , session . Id ) ;
2020-04-28 12:12:06 +00:00
2020-11-16 19:25:13 +00:00
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . NotInGroup , string . Empty ) ;
_sessionManager . SendSyncPlayGroupUpdate ( session , error , CancellationToken . None ) ;
return ;
}
2020-04-28 12:12:06 +00:00
2020-11-16 19:25:13 +00:00
// Group lock required as GroupController is not thread-safe.
lock ( group )
{
2020-05-04 17:46:02 +00:00
group . HandleRequest ( session , request , cancellationToken ) ;
2020-04-01 15:52:42 +00:00
}
}
2020-04-15 16:03:58 +00:00
2020-11-15 16:03:27 +00: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 ;
2020-11-16 19:25:13 +00:00
Guid groupId = FindJoinedGroupId ( session ) ;
if ( groupId . Equals ( Guid . Empty ) )
{
return ;
2020-11-15 16:03:27 +00:00
}
2020-11-16 19:25:13 +00:00
2020-11-28 13:19:24 +00:00
var request = new JoinGroupRequestBody ( )
{
GroupId = groupId
} ;
2020-11-16 19:25:13 +00:00
JoinGroup ( session , groupId , request , CancellationToken . None ) ;
2020-11-15 16:03:27 +00:00
}
/// <summary>
/// Checks if a given session has joined a group.
/// </summary>
/// <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 )
{
2020-11-16 19:25:13 +00:00
lock ( _mapsLock )
{
return _sessionToGroupMap . ContainsKey ( session . Id ) ;
}
2020-11-15 16:03:27 +00:00
}
/// <summary>
/// Gets the group joined by the given session, if any.
/// </summary>
2020-11-16 19:25:13 +00:00
/// <param name="session">The session.</param>
/// <returns>The group.</returns>
private IGroupController FindJoinedGroup ( SessionInfo session )
{
lock ( _mapsLock )
{
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
return group ;
}
}
/// <summary>
/// Gets the group identifier joined by the given session, if any.
/// </summary>
2020-11-15 16:03:27 +00:00
/// <param name="session">The session.</param>
/// <returns>The group identifier if the session has joined a group, an empty identifier otherwise.</returns>
2020-11-16 19:25:13 +00:00
private Guid FindJoinedGroupId ( SessionInfo session )
2020-11-15 16:03:27 +00:00
{
2020-11-16 19:25:13 +00:00
return FindJoinedGroup ( session ) ? . GroupId ? ? Guid . Empty ;
2020-11-15 16:03:27 +00:00
}
/// <summary>
/// Maps a session to a group.
/// </summary>
/// <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 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
if ( session = = null )
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2020-11-16 19:25:13 +00:00
lock ( _mapsLock )
2020-04-01 15:52:42 +00:00
{
2020-11-16 19:25:13 +00:00
if ( IsSessionInGroup ( session ) )
{
throw new InvalidOperationException ( "Session in other group already!" ) ;
}
2020-05-26 09:37:52 +00:00
2020-11-16 19:25:13 +00:00
_sessionToGroupMap [ session . Id ] = group ? ? throw new InvalidOperationException ( "Group is null!" ) ;
}
2020-04-01 15:52:42 +00:00
}
2020-11-15 16:03:27 +00:00
/// <summary>
/// Unmaps a session from a group.
/// </summary>
/// <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 15:52:42 +00:00
{
2020-10-22 13:40:34 +00:00
if ( session = = null )
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
if ( group = = null )
{
throw new InvalidOperationException ( "Group is null!" ) ;
}
2020-11-16 19:25:13 +00:00
lock ( _mapsLock )
2020-04-01 15:52:42 +00:00
{
2020-11-16 19:25:13 +00:00
if ( ! IsSessionInGroup ( session ) )
{
throw new InvalidOperationException ( "Session not in any group!" ) ;
}
2020-04-01 15:52:42 +00:00
2020-11-16 19:25:13 +00:00
_sessionToGroupMap . Remove ( session . Id , out var tempGroup ) ;
if ( ! tempGroup . GroupId . Equals ( group . GroupId ) )
{
throw new InvalidOperationException ( "Session was in wrong group!" ) ;
}
2020-04-01 15:52:42 +00:00
}
}
2020-11-15 16:03:27 +00: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 )
{
2020-11-16 16:40:19 +00:00
_logger . LogWarning ( "Session {SessionId} requested {RequestType} but does not have access to SyncPlay." , session . Id , requestType ) ;
2020-11-15 16:03:27 +00:00
// 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 )
{
2020-11-16 16:40:19 +00:00
_logger . LogWarning ( "Session {SessionId} does not have permission to create groups." , session . Id ) ;
2020-11-15 16:03:27 +00:00
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 15:52:42 +00:00
}
}