< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.SyncPlay.SyncPlayManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
Line coverage
14%
Covered lines: 21
Uncovered lines: 121
Coverable lines: 142
Total lines: 419
Line coverage: 14.7%
Branch coverage
1%
Covered branches: 1
Total branches: 70
Branch coverage: 1.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Dispose()100%11100%
NewGroup(...)0%7280%
JoinGroup(...)0%210140%
LeaveGroup(...)0%156120%
ListGroups(...)0%7280%
GetGroup(...)0%4260%
HandleRequest(...)0%156120%
IsUserActive(...)0%620%
Dispose(...)50%2280%
OnSessionEnded(...)0%620%
UpdateSessionsCounter(...)0%2040%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Threading;
 7using MediaBrowser.Controller.Library;
 8using MediaBrowser.Controller.Session;
 9using MediaBrowser.Controller.SyncPlay;
 10using MediaBrowser.Controller.SyncPlay.Requests;
 11using MediaBrowser.Model.SyncPlay;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Emby.Server.Implementations.SyncPlay
 15{
 16    /// <summary>
 17    /// Class SyncPlayManager.
 18    /// </summary>
 19    public class SyncPlayManager : ISyncPlayManager, IDisposable
 20    {
 21        /// <summary>
 22        /// The logger.
 23        /// </summary>
 24        private readonly ILogger<SyncPlayManager> _logger;
 25
 26        /// <summary>
 27        /// The logger factory.
 28        /// </summary>
 29        private readonly ILoggerFactory _loggerFactory;
 30
 31        /// <summary>
 32        /// The user manager.
 33        /// </summary>
 34        private readonly IUserManager _userManager;
 35
 36        /// <summary>
 37        /// The session manager.
 38        /// </summary>
 39        private readonly ISessionManager _sessionManager;
 40
 41        /// <summary>
 42        /// The library manager.
 43        /// </summary>
 44        private readonly ILibraryManager _libraryManager;
 45
 46        /// <summary>
 47        /// The map between users and counter of active sessions.
 48        /// </summary>
 1649        private readonly ConcurrentDictionary<Guid, int> _activeUsers =
 1650            new ConcurrentDictionary<Guid, int>();
 51
 52        /// <summary>
 53        /// The map between sessions and groups.
 54        /// </summary>
 1655        private readonly ConcurrentDictionary<string, Group> _sessionToGroupMap =
 1656            new ConcurrentDictionary<string, Group>(StringComparer.OrdinalIgnoreCase);
 57
 58        /// <summary>
 59        /// The groups.
 60        /// </summary>
 1661        private readonly ConcurrentDictionary<Guid, Group> _groups =
 1662            new ConcurrentDictionary<Guid, Group>();
 63
 64        /// <summary>
 65        /// Lock used for accessing multiple groups at once.
 66        /// </summary>
 67        /// <remarks>
 68        /// This lock has priority on locks made on <see cref="Group"/>.
 69        /// </remarks>
 1670        private readonly Lock _groupsLock = new();
 71
 72        private bool _disposed = false;
 73
 74        /// <summary>
 75        /// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
 76        /// </summary>
 77        /// <param name="loggerFactory">The logger factory.</param>
 78        /// <param name="userManager">The user manager.</param>
 79        /// <param name="sessionManager">The session manager.</param>
 80        /// <param name="libraryManager">The library manager.</param>
 81        public SyncPlayManager(
 82            ILoggerFactory loggerFactory,
 83            IUserManager userManager,
 84            ISessionManager sessionManager,
 85            ILibraryManager libraryManager)
 86        {
 1687            _loggerFactory = loggerFactory;
 1688            _userManager = userManager;
 1689            _sessionManager = sessionManager;
 1690            _libraryManager = libraryManager;
 1691            _logger = loggerFactory.CreateLogger<SyncPlayManager>();
 1692            _sessionManager.SessionEnded += OnSessionEnded;
 1693        }
 94
 95        /// <inheritdoc />
 96        public void Dispose()
 97        {
 1698            Dispose(true);
 1699            GC.SuppressFinalize(this);
 16100        }
 101
 102        /// <inheritdoc />
 103        public GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
 104        {
 0105            if (session is null)
 106            {
 0107                throw new InvalidOperationException("Session is null!");
 108            }
 109
 0110            if (request is null)
 111            {
 0112                throw new InvalidOperationException("Request is null!");
 113            }
 114
 115            // Locking required to access list of groups.
 116            lock (_groupsLock)
 117            {
 118                // Make sure that session has not joined another group.
 0119                if (_sessionToGroupMap.ContainsKey(session.Id))
 120                {
 0121                    var leaveGroupRequest = new LeaveGroupRequest();
 0122                    LeaveGroup(session, leaveGroupRequest, cancellationToken);
 123                }
 124
 0125                var group = new Group(_loggerFactory, _userManager, _sessionManager, _libraryManager);
 0126                _groups[group.GroupId] = group;
 127
 0128                if (!_sessionToGroupMap.TryAdd(session.Id, group))
 129                {
 0130                    throw new InvalidOperationException("Could not add session to group!");
 131                }
 132
 0133                UpdateSessionsCounter(session.UserId, 1);
 0134                group.CreateGroup(session, request, cancellationToken);
 0135                return group.GetInfo();
 136            }
 0137        }
 138
 139        /// <inheritdoc />
 140        public void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
 141        {
 0142            if (session is null)
 143            {
 0144                throw new InvalidOperationException("Session is null!");
 145            }
 146
 0147            if (request is null)
 148            {
 0149                throw new InvalidOperationException("Request is null!");
 150            }
 151
 0152            var user = _userManager.GetUserById(session.UserId);
 153
 154            // Locking required to access list of groups.
 155            lock (_groupsLock)
 156            {
 0157                _groups.TryGetValue(request.GroupId, out Group group);
 158
 0159                if (group is null)
 160                {
 0161                    _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session
 162
 0163                    var error = new SyncPlayGroupDoesNotExistUpdate(Guid.Empty, string.Empty);
 0164                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 0165                    return;
 166                }
 167
 168                // Group lock required to let other requests end first.
 0169                lock (group)
 170                {
 0171                    if (!group.HasAccessToPlayQueue(user))
 172                    {
 0173                        _logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access t
 174
 0175                        var error = new SyncPlayLibraryAccessDeniedUpdate(group.GroupId, string.Empty);
 0176                        _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 0177                        return;
 178                    }
 179
 0180                    if (_sessionToGroupMap.TryGetValue(session.Id, out var existingGroup))
 181                    {
 0182                        if (existingGroup.GroupId.Equals(request.GroupId))
 183                        {
 184                            // Restore session.
 0185                            UpdateSessionsCounter(session.UserId, 1);
 0186                            group.SessionJoin(session, request, cancellationToken);
 0187                            return;
 188                        }
 189
 0190                        var leaveGroupRequest = new LeaveGroupRequest();
 0191                        LeaveGroup(session, leaveGroupRequest, cancellationToken);
 192                    }
 193
 0194                    if (!_sessionToGroupMap.TryAdd(session.Id, group))
 195                    {
 0196                        throw new InvalidOperationException("Could not add session to group!");
 197                    }
 198
 0199                    UpdateSessionsCounter(session.UserId, 1);
 0200                    group.SessionJoin(session, request, cancellationToken);
 0201                }
 202            }
 0203        }
 204
 205        /// <inheritdoc />
 206        public void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken)
 207        {
 0208            if (session is null)
 209            {
 0210                throw new InvalidOperationException("Session is null!");
 211            }
 212
 0213            if (request is null)
 214            {
 0215                throw new InvalidOperationException("Request is null!");
 216            }
 217
 218            // Locking required to access list of groups.
 219            lock (_groupsLock)
 220            {
 0221                if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
 222                {
 223                    // Group lock required to let other requests end first.
 0224                    lock (group)
 225                    {
 0226                        if (_sessionToGroupMap.TryRemove(session.Id, out var tempGroup))
 227                        {
 0228                            if (!tempGroup.GroupId.Equals(group.GroupId))
 229                            {
 0230                                throw new InvalidOperationException("Session was in wrong group!");
 231                            }
 232                        }
 233                        else
 234                        {
 0235                            throw new InvalidOperationException("Could not remove session from group!");
 236                        }
 237
 0238                        UpdateSessionsCounter(session.UserId, -1);
 0239                        group.SessionLeave(session, request, cancellationToken);
 240
 0241                        if (group.IsGroupEmpty())
 242                        {
 0243                            _logger.LogInformation("Group {GroupId} is empty, removing it.", group.GroupId);
 0244                            _groups.Remove(group.GroupId, out _);
 245                        }
 0246                    }
 247                }
 248                else
 249                {
 0250                    _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
 251
 0252                    var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty);
 0253                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 254                }
 0255            }
 0256        }
 257
 258        /// <inheritdoc />
 259        public List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request)
 260        {
 0261            if (session is null)
 262            {
 0263                throw new InvalidOperationException("Session is null!");
 264            }
 265
 0266            if (request is null)
 267            {
 0268                throw new InvalidOperationException("Request is null!");
 269            }
 270
 0271            var user = _userManager.GetUserById(session.UserId);
 0272            List<GroupInfoDto> list = new List<GroupInfoDto>();
 273
 274            lock (_groupsLock)
 275            {
 0276                foreach (var (_, group) in _groups)
 277                {
 278                    // Locking required as group is not thread-safe.
 0279                    lock (group)
 280                    {
 0281                        if (group.HasAccessToPlayQueue(user))
 282                        {
 0283                            list.Add(group.GetInfo());
 284                        }
 0285                    }
 286                }
 287            }
 288
 0289            return list;
 290        }
 291
 292        /// <inheritdoc />
 293        public GroupInfoDto GetGroup(SessionInfo session, Guid groupId)
 294        {
 0295            ArgumentNullException.ThrowIfNull(session);
 296
 0297            var user = _userManager.GetUserById(session.UserId);
 298
 299            lock (_groupsLock)
 300            {
 0301                foreach (var (_, group) in _groups)
 302                {
 303                    // Locking required as group is not thread-safe.
 0304                    lock (group)
 305                    {
 0306                        if (group.GroupId.Equals(groupId) && group.HasAccessToPlayQueue(user))
 307                        {
 0308                            return group.GetInfo();
 309                        }
 0310                    }
 311                }
 312            }
 313
 0314            return null;
 0315        }
 316
 317        /// <inheritdoc />
 318        public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToke
 319        {
 0320            if (session is null)
 321            {
 0322                throw new InvalidOperationException("Session is null!");
 323            }
 324
 0325            if (request is null)
 326            {
 0327                throw new InvalidOperationException("Request is null!");
 328            }
 329
 0330            if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
 331            {
 332                // Group lock required as Group is not thread-safe.
 0333                lock (group)
 334                {
 335                    // Make sure that session still belongs to this group.
 0336                    if (_sessionToGroupMap.TryGetValue(session.Id, out var checkGroup) && !checkGroup.GroupId.Equals(gro
 337                    {
 338                        // Drop request.
 0339                        return;
 340                    }
 341
 342                    // Drop request if group is empty.
 0343                    if (group.IsGroupEmpty())
 344                    {
 0345                        return;
 346                    }
 347
 348                    // Apply requested changes to group.
 0349                    group.HandleRequest(session, request, cancellationToken);
 0350                }
 351            }
 352            else
 353            {
 0354                _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
 355
 0356                var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty);
 0357                _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 358            }
 0359        }
 360
 361        /// <inheritdoc />
 362        public bool IsUserActive(Guid userId)
 363        {
 0364            if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
 365            {
 0366                return sessionsCounter > 0;
 367            }
 368
 0369            return false;
 370        }
 371
 372        /// <summary>
 373        /// Releases unmanaged and optionally managed resources.
 374        /// </summary>
 375        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 376        protected virtual void Dispose(bool disposing)
 377        {
 16378            if (_disposed)
 379            {
 0380                return;
 381            }
 382
 16383            _sessionManager.SessionEnded -= OnSessionEnded;
 16384            _disposed = true;
 16385        }
 386
 387        private void OnSessionEnded(object sender, SessionEventArgs e)
 388        {
 0389            var session = e.SessionInfo;
 390
 0391            if (_sessionToGroupMap.TryGetValue(session.Id, out _))
 392            {
 0393                var leaveGroupRequest = new LeaveGroupRequest();
 0394                LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
 395            }
 0396        }
 397
 398        private void UpdateSessionsCounter(Guid userId, int toAdd)
 399        {
 400            // Update sessions counter.
 0401            var newSessionsCounter = _activeUsers.AddOrUpdate(
 0402                userId,
 0403                1,
 0404                (_, sessionsCounter) => sessionsCounter + toAdd);
 405
 406            // Should never happen.
 0407            if (newSessionsCounter < 0)
 408            {
 0409                throw new InvalidOperationException("Sessions counter is negative!");
 410            }
 411
 412            // Clean record if user has no more active sessions.
 0413            if (newSessionsCounter == 0)
 414            {
 0415                _activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
 416            }
 0417        }
 418    }
 419}