< 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
15%
Covered lines: 21
Uncovered lines: 116
Coverable lines: 137
Total lines: 393
Line coverage: 15.3%
Branch coverage
1%
Covered branches: 1
Total branches: 64
Branch coverage: 1.5%
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%
HandleRequest(...)0%156120%
IsUserActive(...)0%620%
Dispose(...)50%2.03280%
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>
 1749        private readonly ConcurrentDictionary<Guid, int> _activeUsers =
 1750            new ConcurrentDictionary<Guid, int>();
 51
 52        /// <summary>
 53        /// The map between sessions and groups.
 54        /// </summary>
 1755        private readonly ConcurrentDictionary<string, Group> _sessionToGroupMap =
 1756            new ConcurrentDictionary<string, Group>(StringComparer.OrdinalIgnoreCase);
 57
 58        /// <summary>
 59        /// The groups.
 60        /// </summary>
 1761        private readonly ConcurrentDictionary<Guid, Group> _groups =
 1762            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>
 1770        private readonly object _groupsLock = new object();
 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        {
 1787            _loggerFactory = loggerFactory;
 1788            _userManager = userManager;
 1789            _sessionManager = sessionManager;
 1790            _libraryManager = libraryManager;
 1791            _logger = loggerFactory.CreateLogger<SyncPlayManager>();
 1792            _sessionManager.SessionEnded += OnSessionEnded;
 1793        }
 94
 95        /// <inheritdoc />
 96        public void Dispose()
 97        {
 1798            Dispose(true);
 1799            GC.SuppressFinalize(this);
 17100        }
 101
 102        /// <inheritdoc />
 103        public void 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.
 0116            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            }
 0136        }
 137
 138        /// <inheritdoc />
 139        public void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
 140        {
 0141            if (session is null)
 142            {
 0143                throw new InvalidOperationException("Session is null!");
 144            }
 145
 0146            if (request is null)
 147            {
 0148                throw new InvalidOperationException("Request is null!");
 149            }
 150
 0151            var user = _userManager.GetUserById(session.UserId);
 152
 153            // Locking required to access list of groups.
 0154            lock (_groupsLock)
 155            {
 0156                _groups.TryGetValue(request.GroupId, out Group group);
 157
 0158                if (group is null)
 159                {
 0160                    _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session
 161
 0162                    var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty);
 0163                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 0164                    return;
 165                }
 166
 167                // Group lock required to let other requests end first.
 0168                lock (group)
 169                {
 0170                    if (!group.HasAccessToPlayQueue(user))
 171                    {
 0172                        _logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access t
 173
 0174                        var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.E
 0175                        _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 0176                        return;
 177                    }
 178
 0179                    if (_sessionToGroupMap.TryGetValue(session.Id, out var existingGroup))
 180                    {
 0181                        if (existingGroup.GroupId.Equals(request.GroupId))
 182                        {
 183                            // Restore session.
 0184                            UpdateSessionsCounter(session.UserId, 1);
 0185                            group.SessionJoin(session, request, cancellationToken);
 0186                            return;
 187                        }
 188
 0189                        var leaveGroupRequest = new LeaveGroupRequest();
 0190                        LeaveGroup(session, leaveGroupRequest, cancellationToken);
 191                    }
 192
 0193                    if (!_sessionToGroupMap.TryAdd(session.Id, group))
 194                    {
 0195                        throw new InvalidOperationException("Could not add session to group!");
 196                    }
 197
 0198                    UpdateSessionsCounter(session.UserId, 1);
 0199                    group.SessionJoin(session, request, cancellationToken);
 0200                }
 201            }
 0202        }
 203
 204        /// <inheritdoc />
 205        public void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken)
 206        {
 0207            if (session is null)
 208            {
 0209                throw new InvalidOperationException("Session is null!");
 210            }
 211
 0212            if (request is null)
 213            {
 0214                throw new InvalidOperationException("Request is null!");
 215            }
 216
 217            // Locking required to access list of groups.
 0218            lock (_groupsLock)
 219            {
 0220                if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
 221                {
 222                    // Group lock required to let other requests end first.
 0223                    lock (group)
 224                    {
 0225                        if (_sessionToGroupMap.TryRemove(session.Id, out var tempGroup))
 226                        {
 0227                            if (!tempGroup.GroupId.Equals(group.GroupId))
 228                            {
 0229                                throw new InvalidOperationException("Session was in wrong group!");
 230                            }
 231                        }
 232                        else
 233                        {
 0234                            throw new InvalidOperationException("Could not remove session from group!");
 235                        }
 236
 0237                        UpdateSessionsCounter(session.UserId, -1);
 0238                        group.SessionLeave(session, request, cancellationToken);
 239
 0240                        if (group.IsGroupEmpty())
 241                        {
 0242                            _logger.LogInformation("Group {GroupId} is empty, removing it.", group.GroupId);
 0243                            _groups.Remove(group.GroupId, out _);
 244                        }
 0245                    }
 246                }
 247                else
 248                {
 0249                    _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
 250
 0251                    var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
 0252                    _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 253                }
 0254            }
 0255        }
 256
 257        /// <inheritdoc />
 258        public List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request)
 259        {
 0260            if (session is null)
 261            {
 0262                throw new InvalidOperationException("Session is null!");
 263            }
 264
 0265            if (request is null)
 266            {
 0267                throw new InvalidOperationException("Request is null!");
 268            }
 269
 0270            var user = _userManager.GetUserById(session.UserId);
 0271            List<GroupInfoDto> list = new List<GroupInfoDto>();
 272
 0273            lock (_groupsLock)
 274            {
 0275                foreach (var (_, group) in _groups)
 276                {
 277                    // Locking required as group is not thread-safe.
 0278                    lock (group)
 279                    {
 0280                        if (group.HasAccessToPlayQueue(user))
 281                        {
 0282                            list.Add(group.GetInfo());
 283                        }
 0284                    }
 285                }
 286            }
 287
 0288            return list;
 289        }
 290
 291        /// <inheritdoc />
 292        public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToke
 293        {
 0294            if (session is null)
 295            {
 0296                throw new InvalidOperationException("Session is null!");
 297            }
 298
 0299            if (request is null)
 300            {
 0301                throw new InvalidOperationException("Request is null!");
 302            }
 303
 0304            if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
 305            {
 306                // Group lock required as Group is not thread-safe.
 0307                lock (group)
 308                {
 309                    // Make sure that session still belongs to this group.
 0310                    if (_sessionToGroupMap.TryGetValue(session.Id, out var checkGroup) && !checkGroup.GroupId.Equals(gro
 311                    {
 312                        // Drop request.
 0313                        return;
 314                    }
 315
 316                    // Drop request if group is empty.
 0317                    if (group.IsGroupEmpty())
 318                    {
 0319                        return;
 320                    }
 321
 322                    // Apply requested changes to group.
 0323                    group.HandleRequest(session, request, cancellationToken);
 0324                }
 325            }
 326            else
 327            {
 0328                _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
 329
 0330                var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
 0331                _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
 332            }
 0333        }
 334
 335        /// <inheritdoc />
 336        public bool IsUserActive(Guid userId)
 337        {
 0338            if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
 339            {
 0340                return sessionsCounter > 0;
 341            }
 342
 0343            return false;
 344        }
 345
 346        /// <summary>
 347        /// Releases unmanaged and optionally managed resources.
 348        /// </summary>
 349        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 350        protected virtual void Dispose(bool disposing)
 351        {
 17352            if (_disposed)
 353            {
 0354                return;
 355            }
 356
 17357            _sessionManager.SessionEnded -= OnSessionEnded;
 17358            _disposed = true;
 17359        }
 360
 361        private void OnSessionEnded(object sender, SessionEventArgs e)
 362        {
 0363            var session = e.SessionInfo;
 364
 0365            if (_sessionToGroupMap.TryGetValue(session.Id, out _))
 366            {
 0367                var leaveGroupRequest = new LeaveGroupRequest();
 0368                LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
 369            }
 0370        }
 371
 372        private void UpdateSessionsCounter(Guid userId, int toAdd)
 373        {
 374            // Update sessions counter.
 0375            var newSessionsCounter = _activeUsers.AddOrUpdate(
 0376                userId,
 0377                1,
 0378                (_, sessionsCounter) => sessionsCounter + toAdd);
 379
 380            // Should never happen.
 0381            if (newSessionsCounter < 0)
 382            {
 0383                throw new InvalidOperationException("Sessions counter is negative!");
 384            }
 385
 386            // Clean record if user has no more active sessions.
 0387            if (newSessionsCounter == 0)
 388            {
 0389                _activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
 390            }
 0391        }
 392    }
 393}