2021-05-20 19:28:18 +00:00
#nullable disable
2020-04-01 15:52:42 +00:00
using System ;
2020-12-04 20:27:25 +00:00
using System.Collections.Concurrent ;
2020-04-01 15:52:42 +00:00
using System.Collections.Generic ;
using System.Threading ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
2020-05-06 21:42:53 +00:00
using MediaBrowser.Controller.SyncPlay ;
2020-11-28 15:03:02 +00:00
using MediaBrowser.Controller.SyncPlay.Requests ;
2020-05-06 21:42:53 +00:00
using MediaBrowser.Model.SyncPlay ;
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-12-07 09:33:15 +00:00
/// <summary>
/// The map between users and counter of active sessions.
/// </summary>
private readonly ConcurrentDictionary < Guid , int > _activeUsers =
new ConcurrentDictionary < Guid , int > ( ) ;
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-12-04 20:27:25 +00:00
private readonly ConcurrentDictionary < string , Group > _sessionToGroupMap =
new ConcurrentDictionary < string , Group > ( StringComparer . OrdinalIgnoreCase ) ;
2020-04-01 15:52:42 +00:00
/// <summary>
/// The groups.
/// </summary>
2020-12-04 20:27:25 +00:00
private readonly ConcurrentDictionary < Guid , Group > _groups =
new ConcurrentDictionary < Guid , Group > ( ) ;
2020-04-28 12:12:06 +00:00
/// <summary>
2020-12-04 20:27:25 +00:00
/// Lock used for accessing multiple groups at once.
2020-04-28 12:12:06 +00:00
/// </summary>
2020-11-30 09:03:42 +00:00
/// <remarks>
2020-12-04 20:27:25 +00:00
/// This lock has priority on locks made on <see cref="Group"/>.
2020-11-30 09:03:42 +00:00
/// </remarks>
2020-04-28 12:12:06 +00:00
private readonly object _groupsLock = 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 > ( ) ;
2021-04-24 14:54:42 +00:00
_sessionManager . SessionEnded + = OnSessionEnded ;
2020-04-01 15:52:42 +00:00
}
/// <inheritdoc />
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
/// <inheritdoc />
2020-11-28 15:03:02 +00:00
public void NewGroup ( SessionInfo session , NewGroupRequest request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2022-12-05 14:00:20 +00:00
if ( session is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2022-12-05 14:00:20 +00:00
if ( request is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Request is null!" ) ;
}
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-12-04 20:27:25 +00:00
// Make sure that session has not joined another group.
if ( _sessionToGroupMap . ContainsKey ( session . Id ) )
2020-04-28 12:12:06 +00:00
{
2020-12-04 20:27:25 +00:00
var leaveGroupRequest = new LeaveGroupRequest ( ) ;
LeaveGroup ( session , leaveGroupRequest , cancellationToken ) ;
}
2020-04-01 15:52:42 +00:00
2020-12-04 20:27:25 +00:00
var group = new Group ( _loggerFactory , _userManager , _sessionManager , _libraryManager ) ;
_groups [ group . GroupId ] = group ;
2020-04-01 15:52:42 +00:00
2020-12-04 20:27:25 +00:00
if ( ! _sessionToGroupMap . TryAdd ( session . Id , group ) )
{
throw new InvalidOperationException ( "Could not add session to group!" ) ;
2020-11-16 19:25:13 +00:00
}
2020-12-04 20:27:25 +00:00
2020-12-07 09:33:15 +00:00
UpdateSessionsCounter ( session . UserId , 1 ) ;
2020-12-04 20:27:25 +00:00
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 15:03:02 +00:00
public void JoinGroup ( SessionInfo session , JoinGroupRequest request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2022-12-05 14:00:20 +00:00
if ( session is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2022-12-05 14:00:20 +00:00
if ( request is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Request is null!" ) ;
}
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-12-03 20:01:18 +00:00
_groups . TryGetValue ( request . GroupId , out Group group ) ;
2020-04-01 15:52:42 +00:00
2022-12-05 14:00:20 +00:00
if ( group is null )
2020-04-22 20:05:53 +00:00
{
2020-11-28 15:03:02 +00:00
_logger . LogWarning ( "Session {SessionId} tried to join group {GroupId} that does not exist." , session . Id , request . 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 ) ;
2021-03-28 11:25:40 +00:00
_sessionManager . SendSyncPlayGroupUpdate ( session . Id , error , CancellationToken . None ) ;
2020-04-28 12:12:06 +00:00
return ;
}
2020-04-22 20:05:53 +00:00
2020-12-04 20:27:25 +00:00
// Group lock required to let other requests end first.
lock ( group )
2020-04-28 12:12:06 +00:00
{
2020-12-04 20:27:25 +00:00
if ( ! group . HasAccessToPlayQueue ( user ) )
2020-05-09 12:34:07 +00:00
{
2020-12-04 20:27:25 +00:00
_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 ( ) ) ;
2020-11-16 19:25:13 +00:00
2020-12-04 20:27:25 +00:00
var error = new GroupUpdate < string > ( group . GroupId , GroupUpdateType . LibraryAccessDenied , string . Empty ) ;
2021-03-28 11:25:40 +00:00
_sessionManager . SendSyncPlayGroupUpdate ( session . Id , error , CancellationToken . None ) ;
2020-12-04 20:27:25 +00:00
return ;
}
2020-11-16 19:25:13 +00:00
2020-12-04 20:27:25 +00:00
if ( _sessionToGroupMap . TryGetValue ( session . Id , out var existingGroup ) )
{
if ( existingGroup . GroupId . Equals ( request . GroupId ) )
2020-11-16 19:25:13 +00:00
{
2020-12-04 20:27:25 +00:00
// Restore session.
2020-12-07 09:33:15 +00:00
UpdateSessionsCounter ( session . UserId , 1 ) ;
2020-12-04 20:27:25 +00:00
group . SessionJoin ( session , request , cancellationToken ) ;
return ;
2020-11-16 19:25:13 +00:00
}
2020-12-04 20:27:25 +00:00
var leaveGroupRequest = new LeaveGroupRequest ( ) ;
LeaveGroup ( session , leaveGroupRequest , cancellationToken ) ;
}
if ( ! _sessionToGroupMap . TryAdd ( session . Id , group ) )
{
throw new InvalidOperationException ( "Could not add session to group!" ) ;
2020-05-09 12:34:07 +00:00
}
2020-12-04 20:27:25 +00:00
2020-12-07 09:33:15 +00:00
UpdateSessionsCounter ( session . UserId , 1 ) ;
2020-12-04 20:27:25 +00:00
group . SessionJoin ( session , request , cancellationToken ) ;
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-11-28 15:03:02 +00:00
public void LeaveGroup ( SessionInfo session , LeaveGroupRequest request , CancellationToken cancellationToken )
2020-04-01 15:52:42 +00:00
{
2022-12-05 14:00:20 +00:00
if ( session is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2022-12-05 14:00:20 +00:00
if ( request is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Request is null!" ) ;
}
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-12-04 20:27:25 +00:00
if ( _sessionToGroupMap . TryGetValue ( session . Id , out var group ) )
2020-04-22 20:05:53 +00:00
{
2020-11-16 19:25:13 +00:00
// Group lock required to let other requests end first.
lock ( group )
{
2020-12-04 20:27:25 +00:00
if ( _sessionToGroupMap . TryRemove ( session . Id , out var tempGroup ) )
{
if ( ! tempGroup . GroupId . Equals ( group . GroupId ) )
{
throw new InvalidOperationException ( "Session was in wrong group!" ) ;
}
}
else
{
throw new InvalidOperationException ( "Could not remove session from group!" ) ;
}
2020-12-07 09:33:15 +00:00
UpdateSessionsCounter ( session . UserId , - 1 ) ;
2020-11-28 15:03:02 +00:00
group . SessionLeave ( session , request , cancellationToken ) ;
2020-11-16 19:25:13 +00:00
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-12-04 20:27:25 +00:00
else
{
_logger . LogWarning ( "Session {SessionId} does not belong to any group." , session . Id ) ;
var error = new GroupUpdate < string > ( Guid . Empty , GroupUpdateType . NotInGroup , string . Empty ) ;
2021-03-28 11:25:40 +00:00
_sessionManager . SendSyncPlayGroupUpdate ( session . Id , error , CancellationToken . None ) ;
2020-12-04 20:27:25 +00:00
}
2020-04-01 15:52:42 +00:00
}
}
/// <inheritdoc />
2020-11-28 15:03:02 +00:00
public List < GroupInfoDto > ListGroups ( SessionInfo session , ListGroupsRequest request )
2020-04-01 15:52:42 +00:00
{
2022-12-05 14:00:20 +00:00
if ( session is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Session is null!" ) ;
}
2022-12-05 14:00:20 +00:00
if ( request is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Request is null!" ) ;
}
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
2021-04-30 13:09:36 +00:00
lock ( _groupsLock )
2020-11-15 16:03:27 +00:00
{
2021-04-30 13:09:36 +00:00
foreach ( var ( _ , group ) in _groups )
2020-11-16 19:25:13 +00:00
{
2021-04-30 13:09:36 +00:00
// Locking required as group is not thread-safe.
lock ( group )
2020-11-16 19:25:13 +00:00
{
2021-04-30 13:09:36 +00:00
if ( group . HasAccessToPlayQueue ( user ) )
{
list . Add ( group . GetInfo ( ) ) ;
}
2020-11-16 19:25:13 +00:00
}
}
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
{
2022-12-05 14:00:20 +00:00
if ( session is null )
2020-11-30 09:03:42 +00:00
{
2020-12-04 20:27:25 +00:00
throw new InvalidOperationException ( "Session is null!" ) ;
2020-11-30 09:03:42 +00:00
}
2022-12-05 14:00:20 +00:00
if ( request is null )
2020-12-04 20:27:25 +00:00
{
throw new InvalidOperationException ( "Request is null!" ) ;
}
if ( _sessionToGroupMap . TryGetValue ( session . Id , out var group ) )
{
// Group lock required as Group is not thread-safe.
lock ( group )
{
// Make sure that session still belongs to this group.
if ( _sessionToGroupMap . TryGetValue ( session . Id , out var checkGroup ) & & ! checkGroup . GroupId . Equals ( group . GroupId ) )
{
// Drop request.
return ;
}
// Drop request if group is empty.
if ( group . IsGroupEmpty ( ) )
{
return ;
}
2020-12-04 22:16:15 +00:00
// Apply requested changes to group.
2020-12-04 20:27:25 +00:00
group . HandleRequest ( session , request , cancellationToken ) ;
}
}
else
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 ) ;
2021-03-28 11:25:40 +00:00
_sessionManager . SendSyncPlayGroupUpdate ( session . Id , error , CancellationToken . None ) ;
2020-04-01 15:52:42 +00:00
}
}
2020-04-15 16:03:58 +00:00
2020-12-07 09:33:15 +00:00
/// <inheritdoc />
public bool IsUserActive ( Guid userId )
{
if ( _activeUsers . TryGetValue ( userId , out var sessionsCounter ) )
{
return sessionsCounter > 0 ;
}
2023-04-06 17:38:34 +00:00
return false ;
2020-12-07 09:33:15 +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 ;
}
2021-04-24 14:54:42 +00:00
_sessionManager . SessionEnded - = OnSessionEnded ;
2020-11-15 16:03:27 +00:00
_disposed = true ;
}
2021-04-24 14:54:42 +00:00
private void OnSessionEnded ( object sender , SessionEventArgs e )
2020-11-15 16:03:27 +00:00
{
var session = e . SessionInfo ;
2021-12-15 17:25:36 +00:00
if ( _sessionToGroupMap . TryGetValue ( session . Id , out _ ) )
2020-11-30 09:03:42 +00:00
{
2021-04-24 14:54:42 +00:00
var leaveGroupRequest = new LeaveGroupRequest ( ) ;
LeaveGroup ( session , leaveGroupRequest , CancellationToken . None ) ;
2020-04-01 15:52:42 +00:00
}
}
2020-12-07 09:33:15 +00:00
private void UpdateSessionsCounter ( Guid userId , int toAdd )
{
// Update sessions counter.
var newSessionsCounter = _activeUsers . AddOrUpdate (
userId ,
1 ,
2021-12-15 17:25:36 +00:00
( _ , sessionsCounter ) = > sessionsCounter + toAdd ) ;
2020-12-07 09:33:15 +00:00
// Should never happen.
if ( newSessionsCounter < 0 )
{
throw new InvalidOperationException ( "Sessions counter is negative!" ) ;
}
// Clean record if user has no more active sessions.
if ( newSessionsCounter = = 0 )
{
_activeUsers . TryRemove ( new KeyValuePair < Guid , int > ( userId , newSessionsCounter ) ) ;
}
}
2020-04-01 15:52:42 +00:00
}
}