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