using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Model.SyncPlay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.SyncPlay { /// /// Class SyncPlayManager. /// public class SyncPlayManager : ISyncPlayManager, IDisposable { /// /// The logger. /// private readonly ILogger _logger; /// /// The user manager. /// private readonly IUserManager _userManager; /// /// The session manager. /// private readonly ISessionManager _sessionManager; /// /// The library manager. /// private readonly ILibraryManager _libraryManager; /// /// The map between sessions and groups. /// private readonly Dictionary _sessionToGroupMap = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// The groups. /// private readonly Dictionary _groups = new Dictionary(); /// /// Lock used for accesing any group. /// private readonly object _groupsLock = new object(); private bool _disposed = false; /// /// Initializes a new instance of the class. /// /// The logger. /// The user manager. /// The session manager. /// The library manager. public SyncPlayManager( ILogger logger, IUserManager userManager, ISessionManager sessionManager, ILibraryManager libraryManager) { _logger = logger; _userManager = userManager; _sessionManager = sessionManager; _libraryManager = libraryManager; _sessionManager.SessionStarted += OnSessionManagerSessionStarted; _sessionManager.SessionEnded += OnSessionManagerSessionEnded; _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart; _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; } /// /// Gets all groups. /// /// All groups. public IEnumerable Groups => _groups.Values; /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and optionally managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; _disposed = true; } private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e) { var session = e.SessionInfo; if (!IsSessionInGroup(session)) { return; } var groupId = GetSessionGroup(session) ?? Guid.Empty; var request = new JoinGroupRequest() { GroupId = groupId }; JoinGroup(session, groupId, request, CancellationToken.None); } private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; if (!IsSessionInGroup(session)) { return; } // TODO: probably remove this event, not used at the moment } private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e) { var session = e.Session; if (!IsSessionInGroup(session)) { return; } // TODO: probably remove this event, not used at the moment } private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) { var session = e.Session; if (!IsSessionInGroup(session)) { return; } // TODO: probably remove this event, not used at the moment } private bool IsSessionInGroup(SessionInfo session) { return _sessionToGroupMap.ContainsKey(session.Id); } private Guid? GetSessionGroup(SessionInfo session) { _sessionToGroupMap.TryGetValue(session.Id, out var group); return group?.GroupId; } /// public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) { _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); var error = new GroupUpdate { Type = GroupUpdateType.CreateGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } lock (_groupsLock) { if (IsSessionInGroup(session)) { LeaveGroup(session, cancellationToken); } var group = new SyncPlayGroupController(_logger, _userManager, _sessionManager, _libraryManager, this); _groups[group.GroupId] = group; group.CreateGroup(session, request, cancellationToken); } } /// public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.JoinGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } lock (_groupsLock) { ISyncPlayGroupController group; _groups.TryGetValue(groupId, out group); if (group == null) { _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); var error = new GroupUpdate() { Type = GroupUpdateType.GroupDoesNotExist }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } if (!group.HasAccessToPlayQueue(user)) { _logger.LogWarning("JoinGroup: {0} does not have access to some content from the playing queue of group {1}.", session.Id, group.GroupId.ToString()); var error = new GroupUpdate() { GroupId = group.GroupId.ToString(), Type = GroupUpdateType.LibraryAccessDenied }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } if (IsSessionInGroup(session)) { if (GetSessionGroup(session).Equals(groupId)) { group.SessionRestore(session, request, cancellationToken); return; } LeaveGroup(session, cancellationToken); } group.SessionJoin(session, request, cancellationToken); } } /// public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) { // TODO: determine what happens to users that are in a group and get their permissions revoked lock (_groupsLock) { _sessionToGroupMap.TryGetValue(session.Id, out var group); if (group == null) { _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.NotInGroup }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } group.SessionLeave(session, cancellationToken); if (group.IsGroupEmpty()) { _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GroupId); _groups.Remove(group.GroupId, out _); } } } /// public List ListGroups(SessionInfo session) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { return new List(); } return _groups.Values.Where( group => group.HasAccessToPlayQueue(user)).Select( group => group.GetInfo()).ToList(); } /// public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.JoinGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } lock (_groupsLock) { _sessionToGroupMap.TryGetValue(session.Id, out var group); if (group == null) { _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.NotInGroup }; _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None); return; } group.HandleRequest(session, request, cancellationToken); } } /// public void AddSessionToGroup(SessionInfo session, ISyncPlayGroupController group) { if (IsSessionInGroup(session)) { throw new InvalidOperationException("Session in other group already!"); } _sessionToGroupMap[session.Id] = group; } /// public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayGroupController group) { if (!IsSessionInGroup(session)) { throw new InvalidOperationException("Session not in any group!"); } _sessionToGroupMap.Remove(session.Id, out var tempGroup); if (!tempGroup.GroupId.Equals(group.GroupId)) { throw new InvalidOperationException("Session was in wrong group!"); } } } }