< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Session.SessionManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Session/SessionManager.cs
Line coverage
21%
Covered lines: 212
Uncovered lines: 767
Coverable lines: 979
Total lines: 2143
Line coverage: 21.6%
Branch coverage
12%
Covered branches: 47
Total branches: 390
Branch coverage: 12%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21544/19/2026 - 12:14:27 AM Line coverage: 20.9% (207/987) Branch coverage: 13% (51/390) Total lines: 21544/25/2026 - 12:15:21 AM Line coverage: 20.9% (207/986) Branch coverage: 13% (51/390) Total lines: 21505/4/2026 - 12:15:16 AM Line coverage: 21% (207/985) Branch coverage: 13% (51/390) Total lines: 21495/6/2026 - 12:15:23 AM Line coverage: 21% (207/985) Branch coverage: 12.9% (51/394) Total lines: 21495/7/2026 - 12:15:44 AM Line coverage: 20.9% (207/987) Branch coverage: 12.8% (51/396) Total lines: 21565/20/2026 - 12:15:44 AM Line coverage: 20.9% (207/987) Branch coverage: 11.6% (46/396) Total lines: 21565/27/2026 - 12:15:38 AM Line coverage: 21.2% (210/987) Branch coverage: 11.6% (46/396) Total lines: 21566/1/2026 - 12:16:05 AM Line coverage: 21.3% (209/979) Branch coverage: 11.7% (46/390) Total lines: 21436/8/2026 - 12:16:15 AM Line coverage: 21.6% (212/979) Branch coverage: 12% (47/390) Total lines: 2143 3/6/2026 - 12:14:09 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21544/19/2026 - 12:14:27 AM Line coverage: 20.9% (207/987) Branch coverage: 13% (51/390) Total lines: 21544/25/2026 - 12:15:21 AM Line coverage: 20.9% (207/986) Branch coverage: 13% (51/390) Total lines: 21505/4/2026 - 12:15:16 AM Line coverage: 21% (207/985) Branch coverage: 13% (51/390) Total lines: 21495/6/2026 - 12:15:23 AM Line coverage: 21% (207/985) Branch coverage: 12.9% (51/394) Total lines: 21495/7/2026 - 12:15:44 AM Line coverage: 20.9% (207/987) Branch coverage: 12.8% (51/396) Total lines: 21565/20/2026 - 12:15:44 AM Line coverage: 20.9% (207/987) Branch coverage: 11.6% (46/396) Total lines: 21565/27/2026 - 12:15:38 AM Line coverage: 21.2% (210/987) Branch coverage: 11.6% (46/396) Total lines: 21566/1/2026 - 12:16:05 AM Line coverage: 21.3% (209/979) Branch coverage: 11.7% (46/390) Total lines: 21436/8/2026 - 12:16:15 AM Line coverage: 21.6% (212/979) Branch coverage: 12% (47/390) Total lines: 2143

Coverage delta

Coverage delta 2 -2

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Sessions()100%11100%
OnDeviceManagerDeviceOptionsUpdated(...)0%4260%
CheckDisposed()100%11100%
OnSessionStarted(...)50%44100%
OnSessionEnded()100%210%
UpdateDeviceName(...)0%620%
LogSessionActivity()80%101088.46%
OnSessionControllerConnected(...)100%210%
CloseIfNeededAsync()0%4260%
CloseLiveStreamIfNeededAsync()0%110100%
ReportSessionEnded()0%620%
GetMediaSource(...)100%210%
UpdateNowPlayingItem()0%702260%
RemoveNowPlayingItem(...)0%620%
GetSessionKey(...)100%11100%
GetSessionInfo(...)50%161694.44%
CreateSessionInfo(...)50%141487.5%
GetUsers(...)0%2040%
StartCheckTimers()0%4260%
StopIdleCheckTimer()0%620%
StopInactiveCheckTimer()0%620%
CheckForIdlePlayback()0%110100%
CheckForInactiveSteams()0%4260%
GetNowPlayingItem(...)0%4260%
OnPlaybackStart()0%210140%
OnPlaybackStart(...)0%2040%
OnPlaybackProgress(...)100%210%
UpdateLiveStreamActiveSessionMappings(...)0%110100%
OnPlaybackProgress()0%420200%
OnPlaybackProgress(...)0%4260%
UpdatePlaybackSettings(...)0%272160%
OnPlaybackStopped()0%1190340%
OnPlaybackStopped(...)0%2040%
GetSession(...)0%620%
GetSessionToRemoteControl(...)0%620%
ToSessionInfoDto(...)100%11100%
SendMessageCommand(...)0%620%
SendGeneralCommand(...)0%620%
SendMessageToSession()0%620%
SendMessageToSessions(...)100%11100%
SendPlayCommand()0%812280%
SendSyncPlayCommand()100%210%
SendSyncPlayGroupUpdate()100%210%
TranslateItemForPlayback(...)0%4260%
TranslateItemForInstantMix(...)0%620%
SendBrowseCommand(...)100%210%
SendPlaystateCommand(...)0%2040%
AssertCanControl(...)100%210%
SendRestartRequiredNotification(...)100%210%
AddAdditionalUser(...)0%4260%
RemoveAdditionalUser(...)0%2040%
AuthenticateNewSession(...)100%11100%
AuthenticateDirect(...)100%210%
AuthenticateNewSessionInternal()68.75%161688.63%
GetAuthorizationToken()50%2270.58%
Logout()0%620%
Logout()0%620%
RevokeUserTokens()50%4477.77%
ReportCapabilities(...)100%210%
ReportCapabilities(...)25%9430%
GetItemInfo(...)0%2040%
GetImageCacheTag(...)100%210%
ReportNowViewingItem(...)100%210%
ReportTranscodingInfo(...)0%620%
ClearTranscodingInfo(...)100%210%
GetSession(...)100%210%
GetSessionByAuthenticationToken(...)0%7280%
GetSessionByAuthenticationToken()0%620%
GetSessions(...)0%600240%
SendMessageToAdminSessions(...)0%2040%
SendMessageToUserSessions(...)0%620%
SendMessageToUserSessions(...)100%11100%
SendMessageToUserDeviceSessions(...)100%210%
DisposeAsync()50%13857.14%
OnApplicationStopping()75%4475%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Session/SessionManager.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Linq;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Queries;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Database.Implementations.Entities.Security;
 16using Jellyfin.Database.Implementations.Enums;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Events;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Devices;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Dto;
 26using MediaBrowser.Controller.Entities;
 27using MediaBrowser.Controller.Events;
 28using MediaBrowser.Controller.Events.Authentication;
 29using MediaBrowser.Controller.Events.Session;
 30using MediaBrowser.Controller.Library;
 31using MediaBrowser.Controller.Net;
 32using MediaBrowser.Controller.Session;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.Querying;
 37using MediaBrowser.Model.Session;
 38using MediaBrowser.Model.SyncPlay;
 39using Microsoft.EntityFrameworkCore;
 40using Microsoft.Extensions.Hosting;
 41using Microsoft.Extensions.Logging;
 42using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 43
 44namespace Emby.Server.Implementations.Session
 45{
 46    /// <summary>
 47    /// Class SessionManager.
 48    /// </summary>
 49    public sealed class SessionManager : ISessionManager, IAsyncDisposable
 50    {
 51        private readonly IUserDataManager _userDataManager;
 52        private readonly IServerConfigurationManager _config;
 53        private readonly ILogger<SessionManager> _logger;
 54        private readonly IEventManager _eventManager;
 55        private readonly ILibraryManager _libraryManager;
 56        private readonly IUserManager _userManager;
 57        private readonly IMusicManager _musicManager;
 58        private readonly IDtoService _dtoService;
 59        private readonly IImageProcessor _imageProcessor;
 60        private readonly IMediaSourceManager _mediaSourceManager;
 61        private readonly IServerApplicationHost _appHost;
 62        private readonly IDeviceManager _deviceManager;
 63        private readonly CancellationTokenRegistration _shutdownCallback;
 3164        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 3165            = new(StringComparer.OrdinalIgnoreCase);
 66
 3167        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
 3168            = new(StringComparer.OrdinalIgnoreCase);
 69
 70        private Timer _idleTimer;
 71        private Timer _inactiveTimer;
 72
 73        private DtoOptions _itemInfoDtoOptions;
 74        private bool _disposed;
 75
 76        /// <summary>
 77        /// Initializes a new instance of the <see cref="SessionManager"/> class.
 78        /// </summary>
 79        /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param>
 80        /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param>
 81        /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param>
 82        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</p
 83        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
 84        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
 85        /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param>
 86        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
 87        /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
 88        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
 89        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
 90        /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
 91        /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
 92        public SessionManager(
 93            ILogger<SessionManager> logger,
 94            IEventManager eventManager,
 95            IUserDataManager userDataManager,
 96            IServerConfigurationManager serverConfigurationManager,
 97            ILibraryManager libraryManager,
 98            IUserManager userManager,
 99            IMusicManager musicManager,
 100            IDtoService dtoService,
 101            IImageProcessor imageProcessor,
 102            IServerApplicationHost appHost,
 103            IDeviceManager deviceManager,
 104            IMediaSourceManager mediaSourceManager,
 105            IHostApplicationLifetime hostApplicationLifetime)
 106        {
 31107            _logger = logger;
 31108            _eventManager = eventManager;
 31109            _userDataManager = userDataManager;
 31110            _config = serverConfigurationManager;
 31111            _libraryManager = libraryManager;
 31112            _userManager = userManager;
 31113            _musicManager = musicManager;
 31114            _dtoService = dtoService;
 31115            _imageProcessor = imageProcessor;
 31116            _appHost = appHost;
 31117            _deviceManager = deviceManager;
 31118            _mediaSourceManager = mediaSourceManager;
 31119            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 120
 31121            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 31122        }
 123
 124        /// <summary>
 125        /// Occurs when playback has started.
 126        /// </summary>
 127        public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
 128
 129        /// <summary>
 130        /// Occurs when playback has progressed.
 131        /// </summary>
 132        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
 133
 134        /// <summary>
 135        /// Occurs when playback has stopped.
 136        /// </summary>
 137        public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 138
 139        /// <inheritdoc />
 140        public event EventHandler<SessionEventArgs> SessionStarted;
 141
 142        /// <inheritdoc />
 143        public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 144
 145        /// <inheritdoc />
 146        public event EventHandler<SessionEventArgs> SessionEnded;
 147
 148        /// <inheritdoc />
 149        public event EventHandler<SessionEventArgs> SessionActivity;
 150
 151        /// <inheritdoc />
 152        public event EventHandler<SessionEventArgs> SessionControllerConnected;
 153
 154        /// <summary>
 155        /// Gets all connections.
 156        /// </summary>
 157        /// <value>All connections.</value>
 37158        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate)
 159
 160        private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e
 161        {
 0162            foreach (var session in Sessions)
 163            {
 0164                if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
 165                {
 0166                    if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
 167                    {
 0168                        session.HasCustomDeviceName = true;
 0169                        session.DeviceName = e.Argument.Item2.CustomName;
 170                    }
 171                    else
 172                    {
 0173                        session.HasCustomDeviceName = false;
 174                    }
 175                }
 176            }
 0177        }
 178
 179        private void CheckDisposed()
 180        {
 56181            ObjectDisposedException.ThrowIf(_disposed, this);
 56182        }
 183
 184        private void OnSessionStarted(SessionInfo info)
 185        {
 15186            if (!string.IsNullOrEmpty(info.DeviceId))
 187            {
 15188                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 189
 15190                if (capabilities is not null)
 191                {
 15192                    ReportCapabilities(info, capabilities, false);
 193                }
 194            }
 195
 15196            _eventManager.Publish(new SessionStartedEventArgs(info));
 197
 15198            EventHelper.QueueEventIfNotNull(
 15199                SessionStarted,
 15200                this,
 15201                new SessionEventArgs
 15202                {
 15203                    SessionInfo = info
 15204                },
 15205                _logger);
 15206        }
 207
 208        private async ValueTask OnSessionEnded(SessionInfo info)
 209        {
 0210            EventHelper.QueueEventIfNotNull(
 0211                SessionEnded,
 0212                this,
 0213                new SessionEventArgs
 0214                {
 0215                    SessionInfo = info
 0216                },
 0217                _logger);
 218
 0219            _eventManager.Publish(new SessionEndedEventArgs(info));
 220
 0221            await info.DisposeAsync().ConfigureAwait(false);
 0222        }
 223
 224        /// <inheritdoc />
 225        public void UpdateDeviceName(string sessionId, string reportedDeviceName)
 226        {
 0227            var session = GetSession(sessionId);
 0228            if (session is not null)
 229            {
 0230                session.DeviceName = reportedDeviceName;
 231            }
 0232        }
 233
 234        /// <summary>
 235        /// Logs the user activity.
 236        /// </summary>
 237        /// <param name="appName">Type of the client.</param>
 238        /// <param name="appVersion">The app version.</param>
 239        /// <param name="deviceId">The device id.</param>
 240        /// <param name="deviceName">Name of the device.</param>
 241        /// <param name="remoteEndPoint">The remote end point.</param>
 242        /// <param name="user">The user.</param>
 243        /// <returns>SessionInfo.</returns>
 244        public async Task<SessionInfo> LogSessionActivity(
 245            string appName,
 246            string appVersion,
 247            string deviceId,
 248            string deviceName,
 249            string remoteEndPoint,
 250            User user)
 251        {
 15252            CheckDisposed();
 253
 15254            ArgumentException.ThrowIfNullOrEmpty(appName);
 15255            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 15256            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 257
 15258            var activityDate = DateTime.UtcNow;
 15259            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 15260            var lastActivityDate = session.LastActivityDate;
 15261            session.LastActivityDate = activityDate;
 262
 15263            if (user is not null)
 264            {
 15265                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 266
 15267                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 268                {
 269                    try
 270                    {
 15271                        user.LastActivityDate = activityDate;
 15272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 15273                    }
 0274                    catch (DbUpdateConcurrencyException)
 275                    {
 0276                        _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is 
 0277                    }
 278                }
 279            }
 280
 15281            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 282            {
 15283                SessionActivity?.Invoke(
 15284                    this,
 15285                    new SessionEventArgs
 15286                    {
 15287                        SessionInfo = session
 15288                    });
 289            }
 290
 15291            return session;
 15292        }
 293
 294        /// <inheritdoc />
 295        public void OnSessionControllerConnected(SessionInfo session)
 296        {
 0297            EventHelper.QueueEventIfNotNull(
 0298                SessionControllerConnected,
 0299                this,
 0300                new SessionEventArgs
 0301                {
 0302                    SessionInfo = session
 0303                },
 0304                _logger);
 0305        }
 306
 307        /// <inheritdoc />
 308        public async Task CloseIfNeededAsync(SessionInfo session)
 309        {
 0310            if (!session.SessionControllers.Any(i => i.IsSessionActive))
 311            {
 0312                var key = GetSessionKey(session.Client, session.DeviceId);
 313
 0314                _activeConnections.TryRemove(key, out _);
 0315                if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
 316                {
 0317                    await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false)
 318                }
 319
 0320                await OnSessionEnded(session).ConfigureAwait(false);
 321            }
 0322        }
 323
 324        /// <inheritdoc />
 325        public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
 326        {
 0327            bool liveStreamNeedsToBeClosed = false;
 328
 0329            if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
 330            {
 0331                if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
 332                {
 0333                    if (!string.IsNullOrEmpty(correspondingId))
 334                    {
 0335                        activeSessionMappings.TryRemove(correspondingId, out _);
 336                    }
 337
 0338                    liveStreamNeedsToBeClosed = true;
 339                }
 340
 0341                if (activeSessionMappings.IsEmpty)
 342                {
 0343                    _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
 344                }
 345            }
 346
 0347            if (liveStreamNeedsToBeClosed)
 348            {
 349                try
 350                {
 0351                    await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
 0352                }
 0353                catch (Exception ex)
 354                {
 0355                    _logger.LogError(ex, "Error closing live stream");
 0356                }
 357            }
 0358        }
 359
 360        /// <inheritdoc />
 361        public async ValueTask ReportSessionEnded(string sessionId)
 362        {
 0363            CheckDisposed();
 0364            var session = GetSession(sessionId, false);
 365
 0366            if (session is not null)
 367            {
 0368                var key = GetSessionKey(session.Client, session.DeviceId);
 369
 0370                _activeConnections.TryRemove(key, out _);
 371
 0372                await OnSessionEnded(session).ConfigureAwait(false);
 373            }
 0374        }
 375
 376        private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
 377        {
 0378            return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
 379        }
 380
 381        /// <summary>
 382        /// Updates the now playing item id.
 383        /// </summary>
 384        /// <returns>Task.</returns>
 385        private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bo
 386        {
 0387            if (session is null)
 388            {
 0389                return;
 390            }
 391
 0392            if (string.IsNullOrEmpty(info.MediaSourceId))
 393            {
 0394                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 395            }
 396
 0397            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 398            {
 0399                var current = session.NowPlayingItem;
 400
 0401                if (current is null || !info.ItemId.Equals(current.Id))
 402                {
 0403                    var runtimeTicks = libraryItem.RunTimeTicks;
 404
 0405                    MediaSourceInfo mediaSource = null;
 0406                    if (libraryItem is IHasMediaSources)
 407                    {
 0408                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 409
 0410                        if (mediaSource is not null)
 411                        {
 0412                            runtimeTicks = mediaSource.RunTimeTicks;
 413                        }
 414                    }
 415
 0416                    info.Item = GetItemInfo(libraryItem, mediaSource);
 417
 0418                    info.Item.RunTimeTicks = runtimeTicks;
 419                }
 420                else
 421                {
 0422                    info.Item = current;
 423                }
 424            }
 425
 0426            session.NowPlayingItem = info.Item;
 0427            session.LastActivityDate = DateTime.UtcNow;
 428
 0429            if (updateLastCheckInTime)
 430            {
 0431                session.LastPlaybackCheckIn = DateTime.UtcNow;
 432            }
 433
 0434            if (info.IsPaused && session.LastPausedDate is null)
 435            {
 0436                session.LastPausedDate = DateTime.UtcNow;
 437            }
 0438            else if (!info.IsPaused)
 439            {
 0440                session.LastPausedDate = null;
 441            }
 442
 0443            session.PlayState.IsPaused = info.IsPaused;
 0444            session.PlayState.PositionTicks = info.PositionTicks;
 0445            session.PlayState.MediaSourceId = info.MediaSourceId;
 0446            session.PlayState.LiveStreamId = info.LiveStreamId;
 0447            session.PlayState.CanSeek = info.CanSeek;
 0448            session.PlayState.IsMuted = info.IsMuted;
 0449            session.PlayState.VolumeLevel = info.VolumeLevel;
 0450            session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
 0451            session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0452            session.PlayState.PlayMethod = info.PlayMethod;
 0453            session.PlayState.RepeatMode = info.RepeatMode;
 0454            session.PlayState.PlaybackOrder = info.PlaybackOrder;
 0455            session.PlaylistItemId = info.PlaylistItemId;
 0456        }
 457
 458        /// <summary>
 459        /// Removes the now playing item id.
 460        /// </summary>
 461        /// <param name="session">The session.</param>
 462        private void RemoveNowPlayingItem(SessionInfo session)
 463        {
 0464            session.NowPlayingItem = null;
 0465            session.FullNowPlayingItem = null;
 0466            session.PlayState = new PlayerStateInfo();
 467
 0468            if (!string.IsNullOrEmpty(session.DeviceId))
 469            {
 0470                ClearTranscodingInfo(session.DeviceId);
 471            }
 0472        }
 473
 474        private static string GetSessionKey(string appName, string deviceId)
 15475            => appName + deviceId;
 476
 477        /// <summary>
 478        /// Gets the connection.
 479        /// </summary>
 480        /// <param name="appName">Type of the client.</param>
 481        /// <param name="appVersion">The app version.</param>
 482        /// <param name="deviceId">The device id.</param>
 483        /// <param name="deviceName">Name of the device.</param>
 484        /// <param name="remoteEndPoint">The remote end point.</param>
 485        /// <param name="user">The user.</param>
 486        /// <returns>SessionInfo.</returns>
 487        private SessionInfo GetSessionInfo(
 488            string appName,
 489            string appVersion,
 490            string deviceId,
 491            string deviceName,
 492            string remoteEndPoint,
 493            User user)
 494        {
 15495            CheckDisposed();
 496
 15497            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 498
 15499            var key = GetSessionKey(appName, deviceId);
 15500            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 15501            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 15502            if (ReferenceEquals(newSession, sessionInfo))
 503            {
 15504                OnSessionStarted(newSession);
 505            }
 506
 15507            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15508            sessionInfo.UserName = user?.Username;
 15509            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15510            sessionInfo.RemoteEndPoint = remoteEndPoint;
 15511            sessionInfo.Client = appName;
 512
 15513            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 514            {
 15515                sessionInfo.DeviceName = deviceName;
 516            }
 517
 15518            sessionInfo.ApplicationVersion = appVersion;
 519
 15520            if (user is null)
 521            {
 0522                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 523            }
 524
 15525            return sessionInfo;
 526        }
 527
 528        private SessionInfo CreateSessionInfo(
 529            string key,
 530            string appName,
 531            string appVersion,
 532            string deviceId,
 533            string deviceName,
 534            string remoteEndPoint,
 535            User user)
 536        {
 15537            var sessionInfo = new SessionInfo(this, _logger)
 15538            {
 15539                Client = appName,
 15540                DeviceId = deviceId,
 15541                ApplicationVersion = appVersion,
 15542                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 15543                ServerId = _appHost.SystemId
 15544            };
 545
 15546            var username = user?.Username;
 547
 15548            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15549            sessionInfo.UserName = username;
 15550            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15551            sessionInfo.RemoteEndPoint = remoteEndPoint;
 552
 15553            if (string.IsNullOrEmpty(deviceName))
 554            {
 0555                deviceName = "Network Device";
 556            }
 557
 15558            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 15559            {
 15560                DeviceId = deviceId
 15561            };
 15562            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 563            {
 15564                sessionInfo.DeviceName = deviceName;
 565            }
 566            else
 567            {
 0568                sessionInfo.DeviceName = deviceOptions.CustomName;
 0569                sessionInfo.HasCustomDeviceName = true;
 570            }
 571
 15572            return sessionInfo;
 573        }
 574
 575        private List<User> GetUsers(SessionInfo session)
 576        {
 0577            var users = new List<User>();
 578
 0579            if (session.UserId.IsEmpty())
 580            {
 0581                return users;
 582            }
 583
 0584            var user = _userManager.GetUserById(session.UserId);
 585
 0586            if (user is null)
 587            {
 0588                throw new InvalidOperationException("User not found");
 589            }
 590
 0591            users.Add(user);
 592
 0593            users.AddRange(session.AdditionalUsers
 0594                .Select(i => _userManager.GetUserById(i.UserId))
 0595                .Where(i => i is not null));
 596
 0597            return users;
 598        }
 599
 600        private void StartCheckTimers()
 601        {
 0602            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 603
 0604            if (_config.Configuration.InactiveSessionThreshold > 0)
 605            {
 0606                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 607            }
 608            else
 609            {
 0610                StopInactiveCheckTimer();
 611            }
 0612        }
 613
 614        private void StopIdleCheckTimer()
 615        {
 0616            if (_idleTimer is not null)
 617            {
 0618                _idleTimer.Dispose();
 0619                _idleTimer = null;
 620            }
 0621        }
 622
 623        private void StopInactiveCheckTimer()
 624        {
 0625            if (_inactiveTimer is not null)
 626            {
 0627                _inactiveTimer.Dispose();
 0628                _inactiveTimer = null;
 629            }
 0630        }
 631
 632        private async void CheckForIdlePlayback(object state)
 633        {
 0634            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 0635                .ToList();
 636
 0637            if (playingSessions.Count > 0)
 638            {
 0639                var idle = playingSessions
 0640                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 0641                    .ToList();
 642
 0643                foreach (var session in idle)
 644                {
 0645                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 646
 647                    try
 648                    {
 0649                        await OnPlaybackStopped(new PlaybackStopInfo
 0650                        {
 0651                            Item = session.NowPlayingItem,
 0652                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 0653                            SessionId = session.Id,
 0654                            MediaSourceId = session.PlayState?.MediaSourceId,
 0655                            PositionTicks = session.PlayState?.PositionTicks
 0656                        }).ConfigureAwait(false);
 0657                    }
 0658                    catch (Exception ex)
 659                    {
 0660                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 0661                    }
 662                }
 663            }
 664            else
 665            {
 0666                StopIdleCheckTimer();
 667            }
 0668        }
 669
 670        private async void CheckForInactiveSteams(object state)
 671        {
 0672            var inactiveSessions = Sessions.Where(i =>
 0673                    i.NowPlayingItem is not null
 0674                    && i.PlayState.IsPaused
 0675                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 676
 0677            foreach (var session in inactiveSessions)
 678            {
 0679                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 680
 681                try
 682                {
 0683                    await SendPlaystateCommand(
 0684                        session.Id,
 0685                        session.Id,
 0686                        new PlaystateRequest()
 0687                        {
 0688                            Command = PlaystateCommand.Stop,
 0689                            ControllingUserId = session.UserId.ToString(),
 0690                            SeekPositionTicks = session.PlayState?.PositionTicks
 0691                        },
 0692                        CancellationToken.None).ConfigureAwait(true);
 0693                }
 0694                catch (Exception ex)
 695                {
 0696                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 0697                }
 0698            }
 699
 0700            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 701
 0702            if (!playingSessions)
 703            {
 0704                StopInactiveCheckTimer();
 705            }
 0706        }
 707
 708        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 709        {
 0710            if (session is null)
 711            {
 0712                return null;
 713            }
 714
 0715            var item = session.FullNowPlayingItem;
 0716            if (item is not null && item.Id.Equals(itemId))
 717            {
 0718                return item;
 719            }
 720
 0721            item = _libraryManager.GetItemById(itemId);
 722
 0723            session.FullNowPlayingItem = item;
 724
 0725            return item;
 726        }
 727
 728        /// <summary>
 729        /// Used to report that playback has started for an item.
 730        /// </summary>
 731        /// <param name="info">The info.</param>
 732        /// <returns>Task.</returns>
 733        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 734        public async Task OnPlaybackStart(PlaybackStartInfo info)
 735        {
 0736            CheckDisposed();
 737
 0738            ArgumentNullException.ThrowIfNull(info);
 739
 0740            var session = GetSession(info.SessionId);
 741
 0742            var libraryItem = info.ItemId.IsEmpty()
 0743                ? null
 0744                : GetNowPlayingItem(session, info.ItemId);
 745
 0746            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 747
 0748            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 749            {
 0750                ClearTranscodingInfo(session.DeviceId);
 751            }
 752
 0753            session.StartAutomaticProgress(info);
 754
 0755            var users = GetUsers(session);
 756
 0757            if (libraryItem is not null)
 758            {
 0759                foreach (var user in users)
 760                {
 0761                    OnPlaybackStart(user, libraryItem);
 762                }
 763            }
 764
 0765            if (!string.IsNullOrEmpty(info.LiveStreamId))
 766            {
 0767                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 768            }
 769
 0770            var eventArgs = new PlaybackStartEventArgs
 0771            {
 0772                Item = libraryItem,
 0773                Users = users,
 0774                MediaSourceId = info.MediaSourceId,
 0775                MediaInfo = info.Item,
 0776                DeviceName = session.DeviceName,
 0777                ClientName = session.Client,
 0778                DeviceId = session.DeviceId,
 0779                Session = session,
 0780                PlaybackPositionTicks = info.PositionTicks,
 0781                PlaySessionId = info.PlaySessionId
 0782            };
 783
 0784            if (info.Item is not null)
 785            {
 0786                _logger.LogInformation(
 0787                    "User {0} started playback of '{1}' ({2} {3})",
 0788                    session.UserName,
 0789                    info.Item.Name,
 0790                    session.Client,
 0791                    session.ApplicationVersion);
 792            }
 793
 0794            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 795
 796            // Nothing to save here
 797            // Fire events to inform plugins
 0798            EventHelper.QueueEventIfNotNull(
 0799                PlaybackStart,
 0800                this,
 0801                eventArgs,
 0802                _logger);
 803
 0804            StartCheckTimers();
 0805        }
 806
 807        /// <summary>
 808        /// Called when [playback start].
 809        /// </summary>
 810        /// <param name="user">The user object.</param>
 811        /// <param name="item">The item.</param>
 812        private void OnPlaybackStart(User user, BaseItem item)
 813        {
 0814            var data = _userDataManager.GetUserData(user, item);
 815
 0816            data.PlayCount++;
 0817            data.LastPlayedDate = DateTime.UtcNow;
 818
 0819            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 820            {
 0821                data.Played = true;
 822            }
 823
 0824            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0825        }
 826
 827        /// <inheritdoc />
 828        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 829        {
 0830            return OnPlaybackProgress(info, false);
 831        }
 832
 833        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 834        {
 0835            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 836
 0837            if (!string.IsNullOrEmpty(playSessionId))
 838            {
 0839                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 840                {
 0841                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 842                    {
 0843                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 844                    }
 845
 0846                    activeSessionMappings[sessionId] = playSessionId;
 0847                    activeSessionMappings[playSessionId] = sessionId;
 848                }
 849            }
 850            else
 851            {
 0852                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 853                {
 0854                    activeSessionMappings[sessionId] = string.Empty;
 855                }
 856            }
 0857        }
 858
 859        /// <summary>
 860        /// Used to report playback progress for an item.
 861        /// </summary>
 862        /// <param name="info">The playback progress info.</param>
 863        /// <param name="isAutomated">Whether this is an automated update.</param>
 864        /// <returns>Task.</returns>
 865        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 866        {
 0867            CheckDisposed();
 868
 0869            ArgumentNullException.ThrowIfNull(info);
 870
 0871            var session = GetSession(info.SessionId, false);
 0872            if (session is null)
 873            {
 0874                return;
 875            }
 876
 0877            var libraryItem = info.ItemId.IsEmpty()
 0878                ? null
 0879                : GetNowPlayingItem(session, info.ItemId);
 880
 0881            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 882
 0883            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 884            {
 0885                ClearTranscodingInfo(session.DeviceId);
 886            }
 887
 0888            var users = GetUsers(session);
 889
 890            // only update saved user data on actual check-ins, not automated ones
 0891            if (libraryItem is not null && !isAutomated)
 892            {
 0893                foreach (var user in users)
 894                {
 0895                    OnPlaybackProgress(user, libraryItem, info);
 896                }
 897            }
 898
 0899            if (!string.IsNullOrEmpty(info.LiveStreamId))
 900            {
 0901                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 902            }
 903
 0904            var eventArgs = new PlaybackProgressEventArgs
 0905            {
 0906                Item = libraryItem,
 0907                Users = users,
 0908                PlaybackPositionTicks = session.PlayState.PositionTicks,
 0909                MediaSourceId = session.PlayState.MediaSourceId,
 0910                MediaInfo = info.Item,
 0911                DeviceName = session.DeviceName,
 0912                ClientName = session.Client,
 0913                DeviceId = session.DeviceId,
 0914                IsPaused = info.IsPaused,
 0915                PlaySessionId = info.PlaySessionId,
 0916                IsAutomated = isAutomated,
 0917                Session = session
 0918            };
 919
 0920            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 921
 0922            PlaybackProgress?.Invoke(this, eventArgs);
 923
 0924            if (!isAutomated)
 925            {
 0926                session.StartAutomaticProgress(info);
 927            }
 928
 0929            StartCheckTimers();
 0930        }
 931
 932        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 933        {
 0934            var data = _userDataManager.GetUserData(user, item);
 935
 0936            var positionTicks = info.PositionTicks;
 937
 0938            var changed = false;
 939
 0940            if (positionTicks.HasValue)
 941            {
 0942                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0943                changed = true;
 944            }
 945
 0946            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0947            if (tracksChanged)
 948            {
 0949                changed = true;
 950            }
 951
 0952            if (changed)
 953            {
 0954                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 955            }
 0956        }
 957
 958        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 959        {
 0960            var changed = false;
 961
 0962            if (user.RememberAudioSelections)
 963            {
 0964                if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
 965                {
 0966                    data.AudioStreamIndex = info.AudioStreamIndex;
 0967                    changed = true;
 968                }
 969            }
 970            else
 971            {
 0972                if (data.AudioStreamIndex.HasValue)
 973                {
 0974                    data.AudioStreamIndex = null;
 0975                    changed = true;
 976                }
 977            }
 978
 0979            if (user.RememberSubtitleSelections)
 980            {
 0981                if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 982                {
 0983                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0984                    changed = true;
 985                }
 986            }
 987            else
 988            {
 0989                if (data.SubtitleStreamIndex.HasValue)
 990                {
 0991                    data.SubtitleStreamIndex = null;
 0992                    changed = true;
 993                }
 994            }
 995
 0996            return changed;
 997        }
 998
 999        /// <summary>
 1000        /// Used to report that playback has ended for an item.
 1001        /// </summary>
 1002        /// <param name="info">The info.</param>
 1003        /// <returns>Task.</returns>
 1004        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1005        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1006        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1007        {
 01008            CheckDisposed();
 1009
 01010            ArgumentNullException.ThrowIfNull(info);
 1011
 01012            var session = GetSession(info.SessionId);
 1013
 01014            session.StopAutomaticProgress();
 1015
 01016            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1017            {
 1018                // Ensure live stream is cleaned up before throwing, to prevent tuner
 1019                // resource leaks when stalled clients report a negative PositionTicks.
 01020                if (!string.IsNullOrEmpty(info.LiveStreamId))
 1021                {
 01022                    await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1023                }
 1024
 01025                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1026            }
 1027
 01028            var libraryItem = info.ItemId.IsEmpty()
 01029                ? null
 01030                : GetNowPlayingItem(session, info.ItemId);
 1031
 1032            // Normalize
 01033            if (string.IsNullOrEmpty(info.MediaSourceId))
 1034            {
 01035                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1036            }
 1037
 01038            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1039            {
 01040                var current = session.NowPlayingItem;
 1041
 01042                if (current is null || !info.ItemId.Equals(current.Id))
 1043                {
 01044                    MediaSourceInfo mediaSource = null;
 1045
 01046                    if (libraryItem is IHasMediaSources)
 1047                    {
 01048                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1049                    }
 1050
 01051                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1052                }
 1053                else
 1054                {
 01055                    info.Item = current;
 1056                }
 1057            }
 1058
 01059            if (info.Item is not null)
 1060            {
 01061                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1062
 01063                _logger.LogInformation(
 01064                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 01065                    session.UserName,
 01066                    info.Item.Name,
 01067                    msString,
 01068                    session.Client,
 01069                    session.ApplicationVersion);
 1070            }
 1071
 01072            if (info.NowPlayingQueue is not null)
 1073            {
 01074                session.NowPlayingQueue = info.NowPlayingQueue;
 1075            }
 1076
 01077            session.PlaylistItemId = info.PlaylistItemId;
 1078
 01079            RemoveNowPlayingItem(session);
 1080
 01081            var users = GetUsers(session);
 01082            var playedToCompletion = false;
 1083
 01084            if (libraryItem is not null)
 1085            {
 01086                foreach (var user in users)
 1087                {
 01088                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1089                }
 1090            }
 1091
 01092            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1093            {
 01094                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1095            }
 1096
 01097            var eventArgs = new PlaybackStopEventArgs
 01098            {
 01099                Item = libraryItem,
 01100                Users = users,
 01101                PlaybackPositionTicks = info.PositionTicks,
 01102                PlayedToCompletion = playedToCompletion,
 01103                MediaSourceId = info.MediaSourceId,
 01104                MediaInfo = info.Item,
 01105                DeviceName = session.DeviceName,
 01106                ClientName = session.Client,
 01107                DeviceId = session.DeviceId,
 01108                Session = session,
 01109                PlaySessionId = info.PlaySessionId
 01110            };
 1111
 01112            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1113
 01114            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 01115        }
 1116
 1117        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1118        {
 01119            if (playbackFailed)
 1120            {
 01121                return false;
 1122            }
 1123
 01124            var data = _userDataManager.GetUserData(user, item);
 1125            bool playedToCompletion;
 01126            if (positionTicks.HasValue)
 1127            {
 01128                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1129            }
 1130            else
 1131            {
 1132                // If the client isn't able to report this, then we'll just have to make an assumption
 01133                data.PlayCount++;
 01134                data.Played = item.SupportsPlayedStatus;
 01135                data.PlaybackPositionTicks = 0;
 01136                playedToCompletion = true;
 1137            }
 1138
 01139            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1140
 01141            return playedToCompletion;
 1142        }
 1143
 1144        /// <summary>
 1145        /// Gets the session.
 1146        /// </summary>
 1147        /// <param name="sessionId">The session identifier.</param>
 1148        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1149        /// <returns>SessionInfo.</returns>
 1150        /// <exception cref="ResourceNotFoundException">
 1151        /// No session with an Id equal to <c>sessionId</c> was found
 1152        /// and <c>throwOnMissing</c> is <c>true</c>.
 1153        /// </exception>
 1154        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1155        {
 01156            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01157            if (session is null && throwOnMissing)
 1158            {
 01159                throw new ResourceNotFoundException(
 01160                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1161            }
 1162
 01163            return session;
 1164        }
 1165
 1166        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1167        {
 1168            // Accept either device id or session id
 01169            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1170
 01171            if (session is null)
 1172            {
 01173                throw new ResourceNotFoundException(
 01174                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1175            }
 1176
 01177            return session;
 1178        }
 1179
 1180        /// <inheritdoc />
 1181        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1182        {
 151183            return new SessionInfoDto
 151184            {
 151185                PlayState = sessionInfo.PlayState,
 151186                AdditionalUsers = sessionInfo.AdditionalUsers,
 151187                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151188                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151189                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151190                Id = sessionInfo.Id,
 151191                UserId = sessionInfo.UserId,
 151192                UserName = sessionInfo.UserName,
 151193                Client = sessionInfo.Client,
 151194                LastActivityDate = sessionInfo.LastActivityDate,
 151195                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151196                LastPausedDate = sessionInfo.LastPausedDate,
 151197                DeviceName = sessionInfo.DeviceName,
 151198                DeviceType = sessionInfo.DeviceType,
 151199                NowPlayingItem = sessionInfo.NowPlayingItem,
 151200                NowViewingItem = sessionInfo.NowViewingItem,
 151201                DeviceId = sessionInfo.DeviceId,
 151202                ApplicationVersion = sessionInfo.ApplicationVersion,
 151203                TranscodingInfo = sessionInfo.TranscodingInfo,
 151204                IsActive = sessionInfo.IsActive,
 151205                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151206                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151207                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151208                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151209                PlaylistItemId = sessionInfo.PlaylistItemId,
 151210                ServerId = sessionInfo.ServerId,
 151211                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151212                SupportedCommands = sessionInfo.SupportedCommands
 151213            };
 1214        }
 1215
 1216        /// <inheritdoc />
 1217        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1218        {
 01219            CheckDisposed();
 1220
 01221            var generalCommand = new GeneralCommand
 01222            {
 01223                Name = GeneralCommandType.DisplayMessage
 01224            };
 1225
 01226            generalCommand.Arguments["Header"] = command.Header;
 01227            generalCommand.Arguments["Text"] = command.Text;
 1228
 01229            if (command.TimeoutMs.HasValue)
 1230            {
 01231                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1232            }
 1233
 01234            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1235        }
 1236
 1237        /// <inheritdoc />
 1238        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1239        {
 01240            CheckDisposed();
 1241
 01242            var session = GetSessionToRemoteControl(sessionId);
 1243
 01244            if (!string.IsNullOrEmpty(controllingSessionId))
 1245            {
 01246                var controllingSession = GetSession(controllingSessionId);
 01247                AssertCanControl(session, controllingSession);
 1248            }
 1249
 01250            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1251        }
 1252
 1253        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1254        {
 01255            var controllers = session.SessionControllers;
 01256            var messageId = Guid.NewGuid();
 1257
 01258            foreach (var controller in controllers)
 1259            {
 01260                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1261            }
 01262        }
 1263
 1264        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1265        {
 1266            IEnumerable<Task> GetTasks()
 1267            {
 1268                var messageId = Guid.NewGuid();
 1269                foreach (var session in sessions)
 1270                {
 1271                    var controllers = session.SessionControllers;
 1272                    foreach (var controller in controllers)
 1273                    {
 1274                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1275                    }
 1276                }
 1277            }
 1278
 221279            return Task.WhenAll(GetTasks());
 1280        }
 1281
 1282        /// <inheritdoc />
 1283        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1284        {
 01285            CheckDisposed();
 1286
 01287            var session = GetSessionToRemoteControl(sessionId);
 1288
 01289            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1290
 1291            List<BaseItem> items;
 1292
 01293            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1294            {
 01295                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01296                    .ToList();
 1297
 01298                command.PlayCommand = PlayCommand.PlayNow;
 1299            }
 1300            else
 1301            {
 01302                var list = new List<BaseItem>();
 01303                foreach (var itemId in command.ItemIds)
 1304                {
 01305                    var subItems = TranslateItemForPlayback(itemId, user);
 01306                    list.AddRange(subItems);
 1307                }
 1308
 01309                items = list;
 1310            }
 1311
 01312            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1313            {
 01314                items.Shuffle();
 01315                command.PlayCommand = PlayCommand.PlayNow;
 1316            }
 1317
 01318            command.ItemIds = items.Select(i => i.Id).ToArray();
 1319
 01320            if (user is not null)
 1321            {
 01322                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1323                {
 01324                    throw new ArgumentException(
 01325                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1326                }
 1327            }
 1328
 01329            if (user is not null
 01330                && command.ItemIds.Length == 1
 01331                && user.EnableNextEpisodeAutoPlay
 01332                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1333            {
 01334                var series = episode.Series;
 01335                if (series is not null)
 1336                {
 01337                    var episodes = series.GetEpisodes(
 01338                            user,
 01339                            new DtoOptions(false)
 01340                            {
 01341                                EnableImages = false
 01342                            },
 01343                            user.DisplayMissingEpisodes)
 01344                        .Where(i => !i.IsVirtualItem)
 01345                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01346                        .ToList();
 1347
 01348                    if (episodes.Count > 0)
 1349                    {
 01350                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1351                    }
 1352                }
 1353            }
 1354
 01355            if (!string.IsNullOrEmpty(controllingSessionId))
 1356            {
 01357                var controllingSession = GetSession(controllingSessionId);
 01358                AssertCanControl(session, controllingSession);
 01359                if (!controllingSession.UserId.IsEmpty())
 1360                {
 01361                    command.ControllingUserId = controllingSession.UserId;
 1362                }
 1363            }
 1364
 01365            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 01366        }
 1367
 1368        /// <inheritdoc />
 1369        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1370        {
 01371            CheckDisposed();
 01372            var session = GetSession(sessionId);
 01373            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 01374        }
 1375
 1376        /// <inheritdoc />
 1377        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1378        {
 01379            CheckDisposed();
 01380            var session = GetSession(sessionId);
 01381            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 01382        }
 1383
 1384        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1385        {
 01386            var item = _libraryManager.GetItemById(id);
 1387
 01388            if (item is null)
 1389            {
 01390                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01391                return Array.Empty<BaseItem>();
 1392            }
 1393
 01394            if (item is IItemByName byName)
 1395            {
 01396                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01397                {
 01398                    IsFolder = false,
 01399                    Recursive = true,
 01400                    DtoOptions = new DtoOptions(false)
 01401                    {
 01402                        EnableImages = false,
 01403                        Fields = new[]
 01404                        {
 01405                            ItemFields.SortName
 01406                        }
 01407                    },
 01408                    IsVirtualItem = false,
 01409                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01410                });
 1411            }
 1412
 01413            if (item.IsFolder)
 1414            {
 01415                var folder = (Folder)item;
 1416
 01417                return folder.GetItemList(new InternalItemsQuery(user)
 01418                {
 01419                    Recursive = true,
 01420                    IsFolder = false,
 01421                    DtoOptions = new DtoOptions(false)
 01422                    {
 01423                        EnableImages = false,
 01424                        Fields = new ItemFields[]
 01425                        {
 01426                            ItemFields.SortName
 01427                        }
 01428                    },
 01429                    IsVirtualItem = false,
 01430                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01431                });
 1432            }
 1433
 01434            return new[] { item };
 1435        }
 1436
 1437        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1438        {
 01439            var item = _libraryManager.GetItemById(id);
 1440
 01441            if (item is null)
 1442            {
 01443                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01444                return new List<BaseItem>();
 1445            }
 1446
 01447            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1448        }
 1449
 1450        /// <inheritdoc />
 1451        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1452        {
 01453            var generalCommand = new GeneralCommand
 01454            {
 01455                Name = GeneralCommandType.DisplayContent,
 01456                Arguments =
 01457                {
 01458                    ["ItemId"] = command.ItemId,
 01459                    ["ItemName"] = command.ItemName,
 01460                    ["ItemType"] = command.ItemType.ToString()
 01461                }
 01462            };
 1463
 01464            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1465        }
 1466
 1467        /// <inheritdoc />
 1468        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1469        {
 01470            CheckDisposed();
 1471
 01472            var session = GetSessionToRemoteControl(sessionId);
 1473
 01474            if (!string.IsNullOrEmpty(controllingSessionId))
 1475            {
 01476                var controllingSession = GetSession(controllingSessionId);
 01477                AssertCanControl(session, controllingSession);
 01478                if (!controllingSession.UserId.IsEmpty())
 1479                {
 01480                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1481                }
 1482            }
 1483
 01484            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1485        }
 1486
 1487        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1488        {
 01489            ArgumentNullException.ThrowIfNull(session);
 1490
 01491            ArgumentNullException.ThrowIfNull(controllingSession);
 01492        }
 1493
 1494        /// <summary>
 1495        /// Sends the restart required message.
 1496        /// </summary>
 1497        /// <param name="cancellationToken">The cancellation token.</param>
 1498        /// <returns>Task.</returns>
 1499        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1500        {
 01501            CheckDisposed();
 1502
 01503            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1504        }
 1505
 1506        /// <summary>
 1507        /// Adds the additional user.
 1508        /// </summary>
 1509        /// <param name="sessionId">The session identifier.</param>
 1510        /// <param name="userId">The user identifier.</param>
 1511        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1512        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1513        public void AddAdditionalUser(string sessionId, Guid userId)
 1514        {
 01515            CheckDisposed();
 1516
 01517            var session = GetSession(sessionId);
 1518
 01519            if (session.UserId.Equals(userId))
 1520            {
 01521                throw new ArgumentException("The requested user is already the primary user of the session.");
 1522            }
 1523
 01524            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1525            {
 01526                var user = _userManager.GetUserById(userId);
 01527                var newUser = new SessionUserInfo
 01528                {
 01529                    UserId = userId,
 01530                    UserName = user.Username
 01531                };
 1532
 01533                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1534            }
 01535        }
 1536
 1537        /// <summary>
 1538        /// Removes the additional user.
 1539        /// </summary>
 1540        /// <param name="sessionId">The session identifier.</param>
 1541        /// <param name="userId">The user identifier.</param>
 1542        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1543        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1544        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1545        {
 01546            CheckDisposed();
 1547
 01548            var session = GetSession(sessionId);
 1549
 01550            if (session.UserId.Equals(userId))
 1551            {
 01552                throw new ArgumentException("The requested user is already the primary user of the session.");
 1553            }
 1554
 01555            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1556
 01557            if (user is not null)
 1558            {
 01559                var list = session.AdditionalUsers.ToList();
 01560                list.Remove(user);
 1561
 01562                session.AdditionalUsers = list.ToArray();
 1563            }
 01564        }
 1565
 1566        /// <summary>
 1567        /// Authenticates the new session.
 1568        /// </summary>
 1569        /// <param name="request">The authenticationrequest.</param>
 1570        /// <returns>The authentication result.</returns>
 1571        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1572        {
 151573            return AuthenticateNewSessionInternal(request, true);
 1574        }
 1575
 1576        /// <summary>
 1577        /// Directly authenticates the session without enforcing password.
 1578        /// </summary>
 1579        /// <param name="request">The authentication request.</param>
 1580        /// <returns>The authentication result.</returns>
 1581        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1582        {
 01583            return AuthenticateNewSessionInternal(request, false);
 1584        }
 1585
 1586        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1587        {
 231588            CheckDisposed();
 1589
 231590            ArgumentException.ThrowIfNullOrEmpty(request.App);
 211591            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 191592            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 171593            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1594
 151595            User user = null;
 151596            if (!request.UserId.IsEmpty())
 1597            {
 01598                user = _userManager.GetUserById(request.UserId);
 1599            }
 1600
 151601            user ??= _userManager.GetUserByName(request.Username);
 1602
 151603            if (enforcePassword)
 1604            {
 151605                user = await _userManager.AuthenticateUser(
 151606                    request.Username,
 151607                    request.Password,
 151608                    request.RemoteEndPoint,
 151609                    true).ConfigureAwait(false);
 1610            }
 1611
 151612            if (user is null)
 1613            {
 01614                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01615                throw new AuthenticationException("Invalid username or password entered.");
 1616            }
 1617
 151618            if (!string.IsNullOrEmpty(request.DeviceId)
 151619                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1620            {
 01621                throw new SecurityException("User is not allowed access from this device.");
 1622            }
 1623
 151624            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 151625            int maxActiveSessions = user.MaxActiveSessions;
 151626            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 151627            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1628            {
 01629                throw new SecurityException("User is at their maximum number of sessions.");
 1630            }
 1631
 151632            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1633
 151634            var session = await LogSessionActivity(
 151635                request.App,
 151636                request.AppVersion,
 151637                request.DeviceId,
 151638                request.DeviceName,
 151639                request.RemoteEndPoint,
 151640                user).ConfigureAwait(false);
 1641
 151642            var returnResult = new AuthenticationResult
 151643            {
 151644                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 151645                SessionInfo = ToSessionInfoDto(session),
 151646                AccessToken = token,
 151647                ServerId = _appHost.SystemId
 151648            };
 1649
 151650            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 151651            return returnResult;
 151652        }
 1653
 1654        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1655        {
 1656            // This should be validated above, but if it isn't don't delete all tokens.
 171657            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1658
 151659            var existing = _deviceManager.GetDevices(
 151660                new DeviceQuery
 151661                {
 151662                    DeviceId = deviceId,
 151663                    UserId = user.Id
 151664                }).Items;
 1665
 301666            foreach (var auth in existing)
 1667            {
 1668                try
 1669                {
 1670                    // Logout any existing sessions for the user on this device
 01671                    await Logout(auth).ConfigureAwait(false);
 01672                }
 01673                catch (Exception ex)
 1674                {
 01675                    _logger.LogError(ex, "Error while logging out existing session.");
 01676                }
 1677            }
 1678
 151679            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 151680            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1681
 151682            return device.AccessToken;
 151683        }
 1684
 1685        /// <inheritdoc />
 1686        public async Task Logout(string accessToken)
 1687        {
 01688            CheckDisposed();
 1689
 01690            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1691
 01692            var existing = _deviceManager.GetDevices(
 01693                new DeviceQuery
 01694                {
 01695                    Limit = 1,
 01696                    AccessToken = accessToken
 01697                }).Items;
 1698
 01699            if (existing.Count > 0)
 1700            {
 01701                await Logout(existing[0]).ConfigureAwait(false);
 1702            }
 01703        }
 1704
 1705        /// <inheritdoc />
 1706        public async Task Logout(Device device)
 1707        {
 01708            CheckDisposed();
 1709
 01710            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1711
 01712            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1713
 01714            var sessions = Sessions
 01715                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01716                .ToList();
 1717
 01718            foreach (var session in sessions)
 1719            {
 1720                try
 1721                {
 01722                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01723                }
 01724                catch (Exception ex)
 1725                {
 01726                    _logger.LogError(ex, "Error reporting session ended");
 01727                }
 1728            }
 01729        }
 1730
 1731        /// <inheritdoc />
 1732        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1733        {
 21734            CheckDisposed();
 1735
 21736            var existing = _deviceManager.GetDevices(new DeviceQuery
 21737            {
 21738                UserId = userId
 21739            });
 1740
 41741            foreach (var info in existing.Items)
 1742            {
 01743                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1744                {
 01745                    await Logout(info).ConfigureAwait(false);
 1746                }
 1747            }
 21748        }
 1749
 1750        /// <summary>
 1751        /// Reports the capabilities.
 1752        /// </summary>
 1753        /// <param name="sessionId">The session identifier.</param>
 1754        /// <param name="capabilities">The capabilities.</param>
 1755        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1756        {
 01757            CheckDisposed();
 1758
 01759            var session = GetSession(sessionId);
 1760
 01761            ReportCapabilities(session, capabilities, true);
 01762        }
 1763
 1764        private void ReportCapabilities(
 1765            SessionInfo session,
 1766            ClientCapabilities capabilities,
 1767            bool saveCapabilities)
 1768        {
 151769            session.Capabilities = capabilities;
 1770
 151771            if (saveCapabilities)
 1772            {
 01773                CapabilitiesChanged?.Invoke(
 01774                    this,
 01775                    new SessionEventArgs
 01776                    {
 01777                        SessionInfo = session
 01778                    });
 1779
 01780                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1781            }
 151782        }
 1783
 1784        /// <summary>
 1785        /// Converts a BaseItem to a BaseItemInfo.
 1786        /// </summary>
 1787        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1788        {
 01789            ArgumentNullException.ThrowIfNull(item);
 1790
 01791            var dtoOptions = _itemInfoDtoOptions;
 1792
 01793            if (_itemInfoDtoOptions is null)
 1794            {
 01795                dtoOptions = new DtoOptions
 01796                {
 01797                    AddProgramRecordingInfo = false
 01798                };
 1799
 01800                var fields = dtoOptions.Fields.ToList();
 1801
 01802                fields.Remove(ItemFields.CanDelete);
 01803                fields.Remove(ItemFields.CanDownload);
 01804                fields.Remove(ItemFields.ChildCount);
 01805                fields.Remove(ItemFields.CustomRating);
 01806                fields.Remove(ItemFields.DateLastMediaAdded);
 01807                fields.Remove(ItemFields.DateLastRefreshed);
 01808                fields.Remove(ItemFields.DateLastSaved);
 01809                fields.Remove(ItemFields.DisplayPreferencesId);
 01810                fields.Remove(ItemFields.Etag);
 01811                fields.Remove(ItemFields.ItemCounts);
 01812                fields.Remove(ItemFields.MediaSourceCount);
 01813                fields.Remove(ItemFields.MediaStreams);
 01814                fields.Remove(ItemFields.MediaSources);
 01815                fields.Remove(ItemFields.People);
 01816                fields.Remove(ItemFields.PlayAccess);
 01817                fields.Remove(ItemFields.People);
 01818                fields.Remove(ItemFields.ProductionLocations);
 01819                fields.Remove(ItemFields.RecursiveItemCount);
 01820                fields.Remove(ItemFields.RemoteTrailers);
 01821                fields.Remove(ItemFields.SeasonUserData);
 01822                fields.Remove(ItemFields.Settings);
 01823                fields.Remove(ItemFields.SortName);
 01824                fields.Remove(ItemFields.Tags);
 1825
 01826                dtoOptions.Fields = fields.ToArray();
 1827
 01828                _itemInfoDtoOptions = dtoOptions;
 1829            }
 1830
 01831            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1832
 01833            if (mediaSource is not null)
 1834            {
 01835                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1836            }
 1837
 01838            return info;
 1839        }
 1840
 1841        private string GetImageCacheTag(User user)
 1842        {
 1843            try
 1844            {
 01845                return _imageProcessor.GetImageCacheTag(user);
 1846            }
 01847            catch (Exception e)
 1848            {
 01849                _logger.LogError(e, "Error getting image information for profile image");
 01850                return null;
 1851            }
 01852        }
 1853
 1854        /// <inheritdoc />
 1855        public void ReportNowViewingItem(string sessionId, string itemId)
 1856        {
 01857            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1858
 01859            var item = _libraryManager.GetItemById(new Guid(itemId));
 01860            var session = GetSession(sessionId);
 1861
 01862            session.NowViewingItem = GetItemInfo(item, null);
 01863        }
 1864
 1865        /// <inheritdoc />
 1866        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1867        {
 01868            var session = Sessions.FirstOrDefault(i =>
 01869                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1870
 01871            if (session is not null)
 1872            {
 01873                session.TranscodingInfo = info;
 1874            }
 01875        }
 1876
 1877        /// <inheritdoc />
 1878        public void ClearTranscodingInfo(string deviceId)
 1879        {
 01880            ReportTranscodingInfo(deviceId, null);
 01881        }
 1882
 1883        /// <inheritdoc />
 1884        public SessionInfo GetSession(string deviceId, string client, string version)
 1885        {
 01886            return Sessions.FirstOrDefault(i =>
 01887                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01888                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1889        }
 1890
 1891        /// <inheritdoc />
 1892        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1893        {
 01894            ArgumentNullException.ThrowIfNull(info);
 1895
 01896            var user = info.UserId.IsEmpty()
 01897                ? null
 01898                : _userManager.GetUserById(info.UserId);
 1899
 01900            appVersion = string.IsNullOrEmpty(appVersion)
 01901                ? info.AppVersion
 01902                : appVersion;
 1903
 01904            var deviceName = info.DeviceName;
 01905            var appName = info.AppName;
 1906
 01907            if (string.IsNullOrEmpty(deviceId))
 1908            {
 01909                deviceId = info.DeviceId;
 1910            }
 1911
 1912            // Prevent argument exception
 01913            if (string.IsNullOrEmpty(appVersion))
 1914            {
 01915                appVersion = "1";
 1916            }
 1917
 01918            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1919        }
 1920
 1921        /// <inheritdoc />
 1922        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1923        {
 01924            var items = _deviceManager.GetDevices(new DeviceQuery
 01925            {
 01926                AccessToken = token,
 01927                Limit = 1
 01928            }).Items;
 1929
 01930            if (items.Count == 0)
 1931            {
 01932                return null;
 1933            }
 1934
 01935            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 01936        }
 1937
 1938        /// <inheritdoc/>
 1939        public IReadOnlyList<SessionInfoDto> GetSessions(
 1940            Guid userId,
 1941            string deviceId,
 1942            int? activeWithinSeconds,
 1943            Guid? controllableUserToCheck,
 1944            bool isApiKey)
 1945        {
 01946            var result = Sessions;
 01947            if (!string.IsNullOrEmpty(deviceId))
 1948            {
 01949                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1950            }
 1951
 01952            var userCanControlOthers = false;
 01953            var userIsAdmin = false;
 01954            User user = null;
 1955
 01956            if (isApiKey)
 1957            {
 01958                userCanControlOthers = true;
 01959                userIsAdmin = true;
 1960            }
 01961            else if (!userId.IsEmpty())
 1962            {
 01963                user = _userManager.GetUserById(userId);
 01964                if (user is not null)
 1965                {
 01966                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01967                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1968                }
 1969                else
 1970                {
 01971                    return [];
 1972                }
 1973            }
 1974
 01975            if (!controllableUserToCheck.IsNullOrEmpty())
 1976            {
 01977                result = result.Where(i => i.SupportsRemoteControl);
 1978
 01979                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01980                if (controlledUser is null)
 1981                {
 01982                    return [];
 1983                }
 1984
 01985                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1986                {
 1987                    // Controlled user has device sharing disabled
 01988                    result = result.Where(i => !i.UserId.IsEmpty());
 1989                }
 1990
 01991                if (!userCanControlOthers)
 1992                {
 1993                    // User cannot control other user's sessions, validate user id.
 01994                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 1995                }
 1996
 01997                result = result.Where(i =>
 01998                {
 01999                    if (isApiKey)
 02000                    {
 02001                        return true;
 02002                    }
 02003
 02004                    if (user is null)
 02005                    {
 02006                        return false;
 02007                    }
 02008
 02009                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02010                });
 2011            }
 02012            else if (!userIsAdmin)
 2013            {
 2014                // Request isn't from administrator, limit to "own" sessions.
 02015                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2016            }
 2017
 02018            if (!userIsAdmin)
 2019            {
 2020                // Don't report acceleration type for non-admin users.
 02021                result = result.Select(r =>
 02022                {
 02023                    if (r.TranscodingInfo is not null)
 02024                    {
 02025                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02026                    }
 02027
 02028                    return r;
 02029                });
 2030            }
 2031
 02032            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2033            {
 02034                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02035                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2036            }
 2037
 02038            return result.Select(ToSessionInfoDto).ToList();
 2039        }
 2040
 2041        /// <inheritdoc />
 2042        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2043        {
 02044            CheckDisposed();
 2045
 02046            var adminUserIds = _userManager.GetUsers()
 02047                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02048                .Select(i => i.Id)
 02049                .ToList();
 2050
 02051            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2052        }
 2053
 2054        /// <inheritdoc />
 2055        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2056        {
 02057            CheckDisposed();
 2058
 02059            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2060
 02061            if (sessions.Count == 0)
 2062            {
 02063                return Task.CompletedTask;
 2064            }
 2065
 02066            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2067        }
 2068
 2069        /// <inheritdoc />
 2070        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2071        {
 12072            CheckDisposed();
 2073
 12074            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 12075            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2076        }
 2077
 2078        /// <inheritdoc />
 2079        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2080        {
 02081            CheckDisposed();
 2082
 02083            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2084
 02085            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2086        }
 2087
 2088        /// <inheritdoc />
 2089        public async ValueTask DisposeAsync()
 2090        {
 312091            if (_disposed)
 2092            {
 02093                return;
 2094            }
 2095
 622096            foreach (var session in _activeConnections.Values)
 2097            {
 02098                await session.DisposeAsync().ConfigureAwait(false);
 2099            }
 2100
 312101            if (_idleTimer is not null)
 2102            {
 02103                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02104                _idleTimer = null;
 2105            }
 2106
 312107            if (_inactiveTimer is not null)
 2108            {
 02109                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02110                _inactiveTimer = null;
 2111            }
 2112
 312113            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2114
 312115            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 312116            _disposed = true;
 312117        }
 2118
 2119        private async void OnApplicationStopping()
 2120        {
 212121            _logger.LogInformation("Sending shutdown notifications");
 2122            try
 2123            {
 212124                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2125
 212126                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 212127            }
 02128            catch (Exception ex)
 2129            {
 02130                _logger.LogError(ex, "Error sending server shutdown notifications");
 02131            }
 2132
 2133            // Close open websockets to allow Kestrel to shut down cleanly
 722134            foreach (var session in _activeConnections.Values)
 2135            {
 152136                await session.DisposeAsync().ConfigureAwait(false);
 2137            }
 2138
 212139            _activeConnections.Clear();
 212140            _activeLiveStreamSessions.Clear();
 212141        }
 2142    }
 2143}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<Emby.Server.Implementations.Session.SessionManager>,MediaBrowser.Controller.Events.IEventManager,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.IMusicManager,MediaBrowser.Controller.Dto.IDtoService,MediaBrowser.Controller.Drawing.IImageProcessor,MediaBrowser.Controller.IServerApplicationHost,MediaBrowser.Controller.Devices.IDeviceManager,MediaBrowser.Controller.Library.IMediaSourceManager,Microsoft.Extensions.Hosting.IHostApplicationLifetime)
get_Sessions()
OnDeviceManagerDeviceOptionsUpdated(System.Object,Jellyfin.Data.Events.GenericEventArgs`1<System.Tuple`2<System.String,Jellyfin.Database.Implementations.Entities.Security.DeviceOptions>>)
CheckDisposed()
OnSessionStarted(MediaBrowser.Controller.Session.SessionInfo)
OnSessionEnded()
UpdateDeviceName(System.String,System.String)
LogSessionActivity()
OnSessionControllerConnected(MediaBrowser.Controller.Session.SessionInfo)
CloseIfNeededAsync()
CloseLiveStreamIfNeededAsync()
ReportSessionEnded()
GetMediaSource(MediaBrowser.Controller.Entities.BaseItem,System.String,System.String)
UpdateNowPlayingItem()
RemoveNowPlayingItem(MediaBrowser.Controller.Session.SessionInfo)
GetSessionKey(System.String,System.String)
GetSessionInfo(System.String,System.String,System.String,System.String,System.String,Jellyfin.Database.Implementations.Entities.User)
CreateSessionInfo(System.String,System.String,System.String,System.String,System.String,System.String,Jellyfin.Database.Implementations.Entities.User)
GetUsers(MediaBrowser.Controller.Session.SessionInfo)
StartCheckTimers()
StopIdleCheckTimer()
StopInactiveCheckTimer()
CheckForIdlePlayback()
CheckForInactiveSteams()
GetNowPlayingItem(MediaBrowser.Controller.Session.SessionInfo,System.Guid)
OnPlaybackStart()
OnPlaybackStart(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
OnPlaybackProgress(MediaBrowser.Model.Session.PlaybackProgressInfo)
UpdateLiveStreamActiveSessionMappings(System.String,System.String,System.String)
OnPlaybackProgress()
OnPlaybackProgress(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Session.PlaybackProgressInfo)
UpdatePlaybackSettings(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Model.Session.PlaybackProgressInfo,MediaBrowser.Controller.Entities.UserItemData)
OnPlaybackStopped()
OnPlaybackStopped(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,System.Nullable`1<System.Int64>,System.Boolean)
GetSession(System.String,System.Boolean)
GetSessionToRemoteControl(System.String)
ToSessionInfoDto(MediaBrowser.Controller.Session.SessionInfo)
SendMessageCommand(System.String,System.String,MediaBrowser.Model.Session.MessageCommand,System.Threading.CancellationToken)
SendGeneralCommand(System.String,System.String,MediaBrowser.Model.Session.GeneralCommand,System.Threading.CancellationToken)
SendMessageToSession()
SendMessageToSessions(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Session.SessionInfo>,MediaBrowser.Model.Session.SessionMessageType,T,System.Threading.CancellationToken)
SendPlayCommand()
SendSyncPlayCommand()
SendSyncPlayGroupUpdate()
TranslateItemForPlayback(System.Guid,Jellyfin.Database.Implementations.Entities.User)
TranslateItemForInstantMix(System.Guid,Jellyfin.Database.Implementations.Entities.User)
SendBrowseCommand(System.String,System.String,MediaBrowser.Model.Session.BrowseRequest,System.Threading.CancellationToken)
SendPlaystateCommand(System.String,System.String,MediaBrowser.Model.Session.PlaystateRequest,System.Threading.CancellationToken)
AssertCanControl(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Controller.Session.SessionInfo)
SendRestartRequiredNotification(System.Threading.CancellationToken)
AddAdditionalUser(System.String,System.Guid)
RemoveAdditionalUser(System.String,System.Guid)
AuthenticateNewSession(MediaBrowser.Controller.Session.AuthenticationRequest)
AuthenticateDirect(MediaBrowser.Controller.Session.AuthenticationRequest)
AuthenticateNewSessionInternal()
GetAuthorizationToken()
Logout()
Logout()
RevokeUserTokens()
ReportCapabilities(System.String,MediaBrowser.Model.Session.ClientCapabilities)
ReportCapabilities(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Model.Session.ClientCapabilities,System.Boolean)
GetItemInfo(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceInfo)
GetImageCacheTag(Jellyfin.Database.Implementations.Entities.User)
ReportNowViewingItem(System.String,System.String)
ReportTranscodingInfo(System.String,MediaBrowser.Model.Session.TranscodingInfo)
ClearTranscodingInfo(System.String)
GetSession(System.String,System.String,System.String)
GetSessionByAuthenticationToken(Jellyfin.Database.Implementations.Entities.Security.Device,System.String,System.String,System.String)
GetSessionByAuthenticationToken()
GetSessions(System.Guid,System.String,System.Nullable`1<System.Int32>,System.Nullable`1<System.Guid>,System.Boolean)
SendMessageToAdminSessions(MediaBrowser.Model.Session.SessionMessageType,T,System.Threading.CancellationToken)
SendMessageToUserSessions(System.Collections.Generic.List`1<System.Guid>,MediaBrowser.Model.Session.SessionMessageType,System.Func`1<T>,System.Threading.CancellationToken)
SendMessageToUserSessions(System.Collections.Generic.List`1<System.Guid>,MediaBrowser.Model.Session.SessionMessageType,T,System.Threading.CancellationToken)
SendMessageToUserDeviceSessions(System.String,MediaBrowser.Model.Session.SessionMessageType,T,System.Threading.CancellationToken)
DisposeAsync()
OnApplicationStopping()