diff options
Diffstat (limited to 'Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs')
| -rw-r--r-- | Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs new file mode 100644 index 000000000..2ebeea717 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -0,0 +1,396 @@ +#nullable disable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.SyncPlay.Requests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// <summary> + /// Class SyncPlayManager. + /// </summary> + public class SyncPlayManager : ISyncPlayManager, IDisposable + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<SyncPlayManager> _logger; + + /// <summary> + /// The logger factory. + /// </summary> + private readonly ILoggerFactory _loggerFactory; + + /// <summary> + /// The user manager. + /// </summary> + private readonly IUserManager _userManager; + + /// <summary> + /// The session manager. + /// </summary> + private readonly ISessionManager _sessionManager; + + /// <summary> + /// The library manager. + /// </summary> + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// The map between users and counter of active sessions. + /// </summary> + private readonly ConcurrentDictionary<Guid, int> _activeUsers = + new ConcurrentDictionary<Guid, int>(); + + /// <summary> + /// The map between sessions and groups. + /// </summary> + private readonly ConcurrentDictionary<string, Group> _sessionToGroupMap = + new ConcurrentDictionary<string, Group>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// The groups. + /// </summary> + private readonly ConcurrentDictionary<Guid, Group> _groups = + new ConcurrentDictionary<Guid, Group>(); + + /// <summary> + /// Lock used for accessing multiple groups at once. + /// </summary> + /// <remarks> + /// This lock has priority on locks made on <see cref="Group"/>. + /// </remarks> + private readonly object _groupsLock = new object(); + + private bool _disposed = false; + + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayManager" /> class. + /// </summary> + /// <param name="loggerFactory">The logger factory.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="sessionManager">The session manager.</param> + /// <param name="libraryManager">The library manager.</param> + public SyncPlayManager( + ILoggerFactory loggerFactory, + IUserManager userManager, + ISessionManager sessionManager, + ILibraryManager libraryManager) + { + _loggerFactory = loggerFactory; + _userManager = userManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _logger = loggerFactory.CreateLogger<SyncPlayManager>(); + _sessionManager.SessionEnded += OnSessionEnded; + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <inheritdoc /> + public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) + { + if (session == null) + { + throw new InvalidOperationException("Session is null!"); + } + + if (request == null) + { + throw new InvalidOperationException("Request is null!"); + } + + // Locking required to access list of groups. + lock (_groupsLock) + { + // Make sure that session has not joined another group. + if (_sessionToGroupMap.ContainsKey(session.Id)) + { + var leaveGroupRequest = new LeaveGroupRequest(); + LeaveGroup(session, leaveGroupRequest, cancellationToken); + } + + var group = new Group(_loggerFactory, _userManager, _sessionManager, _libraryManager); + _groups[group.GroupId] = group; + + if (!_sessionToGroupMap.TryAdd(session.Id, group)) + { + throw new InvalidOperationException("Could not add session to group!"); + } + + UpdateSessionsCounter(session.UserId, 1); + group.CreateGroup(session, request, cancellationToken); + } + } + + /// <inheritdoc /> + public void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + if (session == null) + { + throw new InvalidOperationException("Session is null!"); + } + + if (request == null) + { + throw new InvalidOperationException("Request is null!"); + } + + var user = _userManager.GetUserById(session.UserId); + + // Locking required to access list of groups. + lock (_groupsLock) + { + _groups.TryGetValue(request.GroupId, out Group group); + + if (group == null) + { + _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId); + + var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty); + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); + return; + } + + // Group lock required to let other requests end first. + lock (group) + { + 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.Id, error, CancellationToken.None); + return; + } + + if (_sessionToGroupMap.TryGetValue(session.Id, out var existingGroup)) + { + if (existingGroup.GroupId.Equals(request.GroupId)) + { + // Restore session. + UpdateSessionsCounter(session.UserId, 1); + group.SessionJoin(session, request, cancellationToken); + return; + } + + var leaveGroupRequest = new LeaveGroupRequest(); + LeaveGroup(session, leaveGroupRequest, cancellationToken); + } + + if (!_sessionToGroupMap.TryAdd(session.Id, group)) + { + throw new InvalidOperationException("Could not add session to group!"); + } + + UpdateSessionsCounter(session.UserId, 1); + group.SessionJoin(session, request, cancellationToken); + } + } + } + + /// <inheritdoc /> + public void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken) + { + if (session == null) + { + throw new InvalidOperationException("Session is null!"); + } + + if (request == null) + { + throw new InvalidOperationException("Request is null!"); + } + + // Locking required to access list of groups. + lock (_groupsLock) + { + if (_sessionToGroupMap.TryGetValue(session.Id, out var group)) + { + // Group lock required to let other requests end first. + lock (group) + { + 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!"); + } + + UpdateSessionsCounter(session.UserId, -1); + group.SessionLeave(session, request, cancellationToken); + + if (group.IsGroupEmpty()) + { + _logger.LogInformation("Group {GroupId} is empty, removing it.", group.GroupId); + _groups.Remove(group.GroupId, out _); + } + } + } + else + { + _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); + + var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); + return; + } + } + } + + /// <inheritdoc /> + public List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request) + { + if (session == null) + { + throw new InvalidOperationException("Session is null!"); + } + + if (request == null) + { + throw new InvalidOperationException("Request is null!"); + } + + var user = _userManager.GetUserById(session.UserId); + List<GroupInfoDto> list = new List<GroupInfoDto>(); + + lock (_groupsLock) + { + foreach (var (_, group) in _groups) + { + // Locking required as group is not thread-safe. + lock (group) + { + if (group.HasAccessToPlayQueue(user)) + { + list.Add(group.GetInfo()); + } + } + } + } + + return list; + } + + /// <inheritdoc /> + public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken) + { + if (session == null) + { + throw new InvalidOperationException("Session is null!"); + } + + if (request == null) + { + 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; + } + + // Apply requested changes to group. + group.HandleRequest(session, request, cancellationToken); + } + } + else + { + _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); + + var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); + } + } + + /// <inheritdoc /> + public bool IsUserActive(Guid userId) + { + if (_activeUsers.TryGetValue(userId, out var sessionsCounter)) + { + return sessionsCounter > 0; + } + else + { + return false; + } + } + + /// <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.SessionEnded -= OnSessionEnded; + _disposed = true; + } + + private void OnSessionEnded(object sender, SessionEventArgs e) + { + var session = e.SessionInfo; + + if (_sessionToGroupMap.TryGetValue(session.Id, out var group)) + { + var leaveGroupRequest = new LeaveGroupRequest(); + LeaveGroup(session, leaveGroupRequest, CancellationToken.None); + } + } + + private void UpdateSessionsCounter(Guid userId, int toAdd) + { + // Update sessions counter. + var newSessionsCounter = _activeUsers.AddOrUpdate( + userId, + 1, + (key, sessionsCounter) => sessionsCounter + toAdd); + + // 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)); + } + } + } +} |
