< 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
20%
Covered lines: 207
Uncovered lines: 780
Coverable lines: 987
Total lines: 2156
Line coverage: 20.9%
Branch coverage
11%
Covered branches: 46
Total branches: 396
Branch coverage: 11.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/13/2026 - 12:11:21 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: 2156 2/13/2026 - 12:11:21 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: 2156

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()70%111076.92%
OnSessionControllerConnected(...)100%210%
CloseIfNeededAsync()0%4260%
CloseLiveStreamIfNeededAsync()0%110100%
ReportSessionEnded()0%620%
GetMediaSource(...)100%210%
UpdateNowPlayingItem()0%1056320%
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%210%
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>
 36158        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        {
 55181            ObjectDisposedException.ThrowIf(_disposed, this);
 55182        }
 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                    {
 0271                        user.LastActivityDate = activityDate;
 0272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 0273                    }
 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;
 456
 0457            var nowPlayingQueue = info.NowPlayingQueue;
 458
 0459            if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
 460            {
 0461                session.NowPlayingQueue = nowPlayingQueue;
 462
 0463                var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
 0464                session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
 0465                    _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
 0466                    new DtoOptions(true));
 467            }
 0468        }
 469
 470        /// <summary>
 471        /// Removes the now playing item id.
 472        /// </summary>
 473        /// <param name="session">The session.</param>
 474        private void RemoveNowPlayingItem(SessionInfo session)
 475        {
 0476            session.NowPlayingItem = null;
 0477            session.FullNowPlayingItem = null;
 0478            session.PlayState = new PlayerStateInfo();
 479
 0480            if (!string.IsNullOrEmpty(session.DeviceId))
 481            {
 0482                ClearTranscodingInfo(session.DeviceId);
 483            }
 0484        }
 485
 486        private static string GetSessionKey(string appName, string deviceId)
 15487            => appName + deviceId;
 488
 489        /// <summary>
 490        /// Gets the connection.
 491        /// </summary>
 492        /// <param name="appName">Type of the client.</param>
 493        /// <param name="appVersion">The app version.</param>
 494        /// <param name="deviceId">The device id.</param>
 495        /// <param name="deviceName">Name of the device.</param>
 496        /// <param name="remoteEndPoint">The remote end point.</param>
 497        /// <param name="user">The user.</param>
 498        /// <returns>SessionInfo.</returns>
 499        private SessionInfo GetSessionInfo(
 500            string appName,
 501            string appVersion,
 502            string deviceId,
 503            string deviceName,
 504            string remoteEndPoint,
 505            User user)
 506        {
 15507            CheckDisposed();
 508
 15509            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 510
 15511            var key = GetSessionKey(appName, deviceId);
 15512            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 15513            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 15514            if (ReferenceEquals(newSession, sessionInfo))
 515            {
 15516                OnSessionStarted(newSession);
 517            }
 518
 15519            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15520            sessionInfo.UserName = user?.Username;
 15521            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15522            sessionInfo.RemoteEndPoint = remoteEndPoint;
 15523            sessionInfo.Client = appName;
 524
 15525            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 526            {
 15527                sessionInfo.DeviceName = deviceName;
 528            }
 529
 15530            sessionInfo.ApplicationVersion = appVersion;
 531
 15532            if (user is null)
 533            {
 0534                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 535            }
 536
 15537            return sessionInfo;
 538        }
 539
 540        private SessionInfo CreateSessionInfo(
 541            string key,
 542            string appName,
 543            string appVersion,
 544            string deviceId,
 545            string deviceName,
 546            string remoteEndPoint,
 547            User user)
 548        {
 15549            var sessionInfo = new SessionInfo(this, _logger)
 15550            {
 15551                Client = appName,
 15552                DeviceId = deviceId,
 15553                ApplicationVersion = appVersion,
 15554                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 15555                ServerId = _appHost.SystemId
 15556            };
 557
 15558            var username = user?.Username;
 559
 15560            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15561            sessionInfo.UserName = username;
 15562            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15563            sessionInfo.RemoteEndPoint = remoteEndPoint;
 564
 15565            if (string.IsNullOrEmpty(deviceName))
 566            {
 0567                deviceName = "Network Device";
 568            }
 569
 15570            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 15571            {
 15572                DeviceId = deviceId
 15573            };
 15574            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 575            {
 15576                sessionInfo.DeviceName = deviceName;
 577            }
 578            else
 579            {
 0580                sessionInfo.DeviceName = deviceOptions.CustomName;
 0581                sessionInfo.HasCustomDeviceName = true;
 582            }
 583
 15584            return sessionInfo;
 585        }
 586
 587        private List<User> GetUsers(SessionInfo session)
 588        {
 0589            var users = new List<User>();
 590
 0591            if (session.UserId.IsEmpty())
 592            {
 0593                return users;
 594            }
 595
 0596            var user = _userManager.GetUserById(session.UserId);
 597
 0598            if (user is null)
 599            {
 0600                throw new InvalidOperationException("User not found");
 601            }
 602
 0603            users.Add(user);
 604
 0605            users.AddRange(session.AdditionalUsers
 0606                .Select(i => _userManager.GetUserById(i.UserId))
 0607                .Where(i => i is not null));
 608
 0609            return users;
 610        }
 611
 612        private void StartCheckTimers()
 613        {
 0614            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 615
 0616            if (_config.Configuration.InactiveSessionThreshold > 0)
 617            {
 0618                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 619            }
 620            else
 621            {
 0622                StopInactiveCheckTimer();
 623            }
 0624        }
 625
 626        private void StopIdleCheckTimer()
 627        {
 0628            if (_idleTimer is not null)
 629            {
 0630                _idleTimer.Dispose();
 0631                _idleTimer = null;
 632            }
 0633        }
 634
 635        private void StopInactiveCheckTimer()
 636        {
 0637            if (_inactiveTimer is not null)
 638            {
 0639                _inactiveTimer.Dispose();
 0640                _inactiveTimer = null;
 641            }
 0642        }
 643
 644        private async void CheckForIdlePlayback(object state)
 645        {
 0646            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 0647                .ToList();
 648
 0649            if (playingSessions.Count > 0)
 650            {
 0651                var idle = playingSessions
 0652                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 0653                    .ToList();
 654
 0655                foreach (var session in idle)
 656                {
 0657                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 658
 659                    try
 660                    {
 0661                        await OnPlaybackStopped(new PlaybackStopInfo
 0662                        {
 0663                            Item = session.NowPlayingItem,
 0664                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 0665                            SessionId = session.Id,
 0666                            MediaSourceId = session.PlayState?.MediaSourceId,
 0667                            PositionTicks = session.PlayState?.PositionTicks
 0668                        }).ConfigureAwait(false);
 0669                    }
 0670                    catch (Exception ex)
 671                    {
 0672                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 0673                    }
 674                }
 675            }
 676            else
 677            {
 0678                StopIdleCheckTimer();
 679            }
 0680        }
 681
 682        private async void CheckForInactiveSteams(object state)
 683        {
 0684            var inactiveSessions = Sessions.Where(i =>
 0685                    i.NowPlayingItem is not null
 0686                    && i.PlayState.IsPaused
 0687                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 688
 0689            foreach (var session in inactiveSessions)
 690            {
 0691                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 692
 693                try
 694                {
 0695                    await SendPlaystateCommand(
 0696                        session.Id,
 0697                        session.Id,
 0698                        new PlaystateRequest()
 0699                        {
 0700                            Command = PlaystateCommand.Stop,
 0701                            ControllingUserId = session.UserId.ToString(),
 0702                            SeekPositionTicks = session.PlayState?.PositionTicks
 0703                        },
 0704                        CancellationToken.None).ConfigureAwait(true);
 0705                }
 0706                catch (Exception ex)
 707                {
 0708                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 0709                }
 0710            }
 711
 0712            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 713
 0714            if (!playingSessions)
 715            {
 0716                StopInactiveCheckTimer();
 717            }
 0718        }
 719
 720        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 721        {
 0722            if (session is null)
 723            {
 0724                return null;
 725            }
 726
 0727            var item = session.FullNowPlayingItem;
 0728            if (item is not null && item.Id.Equals(itemId))
 729            {
 0730                return item;
 731            }
 732
 0733            item = _libraryManager.GetItemById(itemId);
 734
 0735            session.FullNowPlayingItem = item;
 736
 0737            return item;
 738        }
 739
 740        /// <summary>
 741        /// Used to report that playback has started for an item.
 742        /// </summary>
 743        /// <param name="info">The info.</param>
 744        /// <returns>Task.</returns>
 745        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 746        public async Task OnPlaybackStart(PlaybackStartInfo info)
 747        {
 0748            CheckDisposed();
 749
 0750            ArgumentNullException.ThrowIfNull(info);
 751
 0752            var session = GetSession(info.SessionId);
 753
 0754            var libraryItem = info.ItemId.IsEmpty()
 0755                ? null
 0756                : GetNowPlayingItem(session, info.ItemId);
 757
 0758            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 759
 0760            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 761            {
 0762                ClearTranscodingInfo(session.DeviceId);
 763            }
 764
 0765            session.StartAutomaticProgress(info);
 766
 0767            var users = GetUsers(session);
 768
 0769            if (libraryItem is not null)
 770            {
 0771                foreach (var user in users)
 772                {
 0773                    OnPlaybackStart(user, libraryItem);
 774                }
 775            }
 776
 0777            if (!string.IsNullOrEmpty(info.LiveStreamId))
 778            {
 0779                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 780            }
 781
 0782            var eventArgs = new PlaybackStartEventArgs
 0783            {
 0784                Item = libraryItem,
 0785                Users = users,
 0786                MediaSourceId = info.MediaSourceId,
 0787                MediaInfo = info.Item,
 0788                DeviceName = session.DeviceName,
 0789                ClientName = session.Client,
 0790                DeviceId = session.DeviceId,
 0791                Session = session,
 0792                PlaybackPositionTicks = info.PositionTicks,
 0793                PlaySessionId = info.PlaySessionId
 0794            };
 795
 0796            if (info.Item is not null)
 797            {
 0798                _logger.LogInformation(
 0799                    "User {0} started playback of '{1}' ({2} {3})",
 0800                    session.UserName,
 0801                    info.Item.Name,
 0802                    session.Client,
 0803                    session.ApplicationVersion);
 804            }
 805
 0806            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 807
 808            // Nothing to save here
 809            // Fire events to inform plugins
 0810            EventHelper.QueueEventIfNotNull(
 0811                PlaybackStart,
 0812                this,
 0813                eventArgs,
 0814                _logger);
 815
 0816            StartCheckTimers();
 0817        }
 818
 819        /// <summary>
 820        /// Called when [playback start].
 821        /// </summary>
 822        /// <param name="user">The user object.</param>
 823        /// <param name="item">The item.</param>
 824        private void OnPlaybackStart(User user, BaseItem item)
 825        {
 0826            var data = _userDataManager.GetUserData(user, item);
 827
 0828            data.PlayCount++;
 0829            data.LastPlayedDate = DateTime.UtcNow;
 830
 0831            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 832            {
 0833                data.Played = true;
 834            }
 835
 0836            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0837        }
 838
 839        /// <inheritdoc />
 840        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 841        {
 0842            return OnPlaybackProgress(info, false);
 843        }
 844
 845        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 846        {
 0847            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 848
 0849            if (!string.IsNullOrEmpty(playSessionId))
 850            {
 0851                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 852                {
 0853                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 854                    {
 0855                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 856                    }
 857
 0858                    activeSessionMappings[sessionId] = playSessionId;
 0859                    activeSessionMappings[playSessionId] = sessionId;
 860                }
 861            }
 862            else
 863            {
 0864                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 865                {
 0866                    activeSessionMappings[sessionId] = string.Empty;
 867                }
 868            }
 0869        }
 870
 871        /// <summary>
 872        /// Used to report playback progress for an item.
 873        /// </summary>
 874        /// <param name="info">The playback progress info.</param>
 875        /// <param name="isAutomated">Whether this is an automated update.</param>
 876        /// <returns>Task.</returns>
 877        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 878        {
 0879            CheckDisposed();
 880
 0881            ArgumentNullException.ThrowIfNull(info);
 882
 0883            var session = GetSession(info.SessionId, false);
 0884            if (session is null)
 885            {
 0886                return;
 887            }
 888
 0889            var libraryItem = info.ItemId.IsEmpty()
 0890                ? null
 0891                : GetNowPlayingItem(session, info.ItemId);
 892
 0893            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 894
 0895            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 896            {
 0897                ClearTranscodingInfo(session.DeviceId);
 898            }
 899
 0900            var users = GetUsers(session);
 901
 902            // only update saved user data on actual check-ins, not automated ones
 0903            if (libraryItem is not null && !isAutomated)
 904            {
 0905                foreach (var user in users)
 906                {
 0907                    OnPlaybackProgress(user, libraryItem, info);
 908                }
 909            }
 910
 0911            if (!string.IsNullOrEmpty(info.LiveStreamId))
 912            {
 0913                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 914            }
 915
 0916            var eventArgs = new PlaybackProgressEventArgs
 0917            {
 0918                Item = libraryItem,
 0919                Users = users,
 0920                PlaybackPositionTicks = session.PlayState.PositionTicks,
 0921                MediaSourceId = session.PlayState.MediaSourceId,
 0922                MediaInfo = info.Item,
 0923                DeviceName = session.DeviceName,
 0924                ClientName = session.Client,
 0925                DeviceId = session.DeviceId,
 0926                IsPaused = info.IsPaused,
 0927                PlaySessionId = info.PlaySessionId,
 0928                IsAutomated = isAutomated,
 0929                Session = session
 0930            };
 931
 0932            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 933
 0934            PlaybackProgress?.Invoke(this, eventArgs);
 935
 0936            if (!isAutomated)
 937            {
 0938                session.StartAutomaticProgress(info);
 939            }
 940
 0941            StartCheckTimers();
 0942        }
 943
 944        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 945        {
 0946            var data = _userDataManager.GetUserData(user, item);
 947
 0948            var positionTicks = info.PositionTicks;
 949
 0950            var changed = false;
 951
 0952            if (positionTicks.HasValue)
 953            {
 0954                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0955                changed = true;
 956            }
 957
 0958            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0959            if (tracksChanged)
 960            {
 0961                changed = true;
 962            }
 963
 0964            if (changed)
 965            {
 0966                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 967            }
 0968        }
 969
 970        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 971        {
 0972            var changed = false;
 973
 0974            if (user.RememberAudioSelections)
 975            {
 0976                if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
 977                {
 0978                    data.AudioStreamIndex = info.AudioStreamIndex;
 0979                    changed = true;
 980                }
 981            }
 982            else
 983            {
 0984                if (data.AudioStreamIndex.HasValue)
 985                {
 0986                    data.AudioStreamIndex = null;
 0987                    changed = true;
 988                }
 989            }
 990
 0991            if (user.RememberSubtitleSelections)
 992            {
 0993                if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 994                {
 0995                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0996                    changed = true;
 997                }
 998            }
 999            else
 1000            {
 01001                if (data.SubtitleStreamIndex.HasValue)
 1002                {
 01003                    data.SubtitleStreamIndex = null;
 01004                    changed = true;
 1005                }
 1006            }
 1007
 01008            return changed;
 1009        }
 1010
 1011        /// <summary>
 1012        /// Used to report that playback has ended for an item.
 1013        /// </summary>
 1014        /// <param name="info">The info.</param>
 1015        /// <returns>Task.</returns>
 1016        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1017        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1018        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1019        {
 01020            CheckDisposed();
 1021
 01022            ArgumentNullException.ThrowIfNull(info);
 1023
 01024            var session = GetSession(info.SessionId);
 1025
 01026            session.StopAutomaticProgress();
 1027
 01028            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1029            {
 1030                // Ensure live stream is cleaned up before throwing, to prevent tuner
 1031                // resource leaks when stalled clients report a negative PositionTicks.
 01032                if (!string.IsNullOrEmpty(info.LiveStreamId))
 1033                {
 01034                    await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1035                }
 1036
 01037                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1038            }
 1039
 01040            var libraryItem = info.ItemId.IsEmpty()
 01041                ? null
 01042                : GetNowPlayingItem(session, info.ItemId);
 1043
 1044            // Normalize
 01045            if (string.IsNullOrEmpty(info.MediaSourceId))
 1046            {
 01047                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1048            }
 1049
 01050            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1051            {
 01052                var current = session.NowPlayingItem;
 1053
 01054                if (current is null || !info.ItemId.Equals(current.Id))
 1055                {
 01056                    MediaSourceInfo mediaSource = null;
 1057
 01058                    if (libraryItem is IHasMediaSources)
 1059                    {
 01060                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1061                    }
 1062
 01063                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1064                }
 1065                else
 1066                {
 01067                    info.Item = current;
 1068                }
 1069            }
 1070
 01071            if (info.Item is not null)
 1072            {
 01073                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1074
 01075                _logger.LogInformation(
 01076                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 01077                    session.UserName,
 01078                    info.Item.Name,
 01079                    msString,
 01080                    session.Client,
 01081                    session.ApplicationVersion);
 1082            }
 1083
 01084            if (info.NowPlayingQueue is not null)
 1085            {
 01086                session.NowPlayingQueue = info.NowPlayingQueue;
 1087            }
 1088
 01089            session.PlaylistItemId = info.PlaylistItemId;
 1090
 01091            RemoveNowPlayingItem(session);
 1092
 01093            var users = GetUsers(session);
 01094            var playedToCompletion = false;
 1095
 01096            if (libraryItem is not null)
 1097            {
 01098                foreach (var user in users)
 1099                {
 01100                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1101                }
 1102            }
 1103
 01104            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1105            {
 01106                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1107            }
 1108
 01109            var eventArgs = new PlaybackStopEventArgs
 01110            {
 01111                Item = libraryItem,
 01112                Users = users,
 01113                PlaybackPositionTicks = info.PositionTicks,
 01114                PlayedToCompletion = playedToCompletion,
 01115                MediaSourceId = info.MediaSourceId,
 01116                MediaInfo = info.Item,
 01117                DeviceName = session.DeviceName,
 01118                ClientName = session.Client,
 01119                DeviceId = session.DeviceId,
 01120                Session = session,
 01121                PlaySessionId = info.PlaySessionId
 01122            };
 1123
 01124            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1125
 01126            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 01127        }
 1128
 1129        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1130        {
 01131            if (playbackFailed)
 1132            {
 01133                return false;
 1134            }
 1135
 01136            var data = _userDataManager.GetUserData(user, item);
 1137            bool playedToCompletion;
 01138            if (positionTicks.HasValue)
 1139            {
 01140                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1141            }
 1142            else
 1143            {
 1144                // If the client isn't able to report this, then we'll just have to make an assumption
 01145                data.PlayCount++;
 01146                data.Played = item.SupportsPlayedStatus;
 01147                data.PlaybackPositionTicks = 0;
 01148                playedToCompletion = true;
 1149            }
 1150
 01151            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1152
 01153            return playedToCompletion;
 1154        }
 1155
 1156        /// <summary>
 1157        /// Gets the session.
 1158        /// </summary>
 1159        /// <param name="sessionId">The session identifier.</param>
 1160        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1161        /// <returns>SessionInfo.</returns>
 1162        /// <exception cref="ResourceNotFoundException">
 1163        /// No session with an Id equal to <c>sessionId</c> was found
 1164        /// and <c>throwOnMissing</c> is <c>true</c>.
 1165        /// </exception>
 1166        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1167        {
 01168            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01169            if (session is null && throwOnMissing)
 1170            {
 01171                throw new ResourceNotFoundException(
 01172                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1173            }
 1174
 01175            return session;
 1176        }
 1177
 1178        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1179        {
 1180            // Accept either device id or session id
 01181            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1182
 01183            if (session is null)
 1184            {
 01185                throw new ResourceNotFoundException(
 01186                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1187            }
 1188
 01189            return session;
 1190        }
 1191
 1192        /// <inheritdoc />
 1193        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1194        {
 151195            return new SessionInfoDto
 151196            {
 151197                PlayState = sessionInfo.PlayState,
 151198                AdditionalUsers = sessionInfo.AdditionalUsers,
 151199                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151200                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151201                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151202                Id = sessionInfo.Id,
 151203                UserId = sessionInfo.UserId,
 151204                UserName = sessionInfo.UserName,
 151205                Client = sessionInfo.Client,
 151206                LastActivityDate = sessionInfo.LastActivityDate,
 151207                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151208                LastPausedDate = sessionInfo.LastPausedDate,
 151209                DeviceName = sessionInfo.DeviceName,
 151210                DeviceType = sessionInfo.DeviceType,
 151211                NowPlayingItem = sessionInfo.NowPlayingItem,
 151212                NowViewingItem = sessionInfo.NowViewingItem,
 151213                DeviceId = sessionInfo.DeviceId,
 151214                ApplicationVersion = sessionInfo.ApplicationVersion,
 151215                TranscodingInfo = sessionInfo.TranscodingInfo,
 151216                IsActive = sessionInfo.IsActive,
 151217                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151218                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151219                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151220                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
 151221                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151222                PlaylistItemId = sessionInfo.PlaylistItemId,
 151223                ServerId = sessionInfo.ServerId,
 151224                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151225                SupportedCommands = sessionInfo.SupportedCommands
 151226            };
 1227        }
 1228
 1229        /// <inheritdoc />
 1230        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1231        {
 01232            CheckDisposed();
 1233
 01234            var generalCommand = new GeneralCommand
 01235            {
 01236                Name = GeneralCommandType.DisplayMessage
 01237            };
 1238
 01239            generalCommand.Arguments["Header"] = command.Header;
 01240            generalCommand.Arguments["Text"] = command.Text;
 1241
 01242            if (command.TimeoutMs.HasValue)
 1243            {
 01244                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1245            }
 1246
 01247            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1248        }
 1249
 1250        /// <inheritdoc />
 1251        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1252        {
 01253            CheckDisposed();
 1254
 01255            var session = GetSessionToRemoteControl(sessionId);
 1256
 01257            if (!string.IsNullOrEmpty(controllingSessionId))
 1258            {
 01259                var controllingSession = GetSession(controllingSessionId);
 01260                AssertCanControl(session, controllingSession);
 1261            }
 1262
 01263            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1264        }
 1265
 1266        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1267        {
 01268            var controllers = session.SessionControllers;
 01269            var messageId = Guid.NewGuid();
 1270
 01271            foreach (var controller in controllers)
 1272            {
 01273                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1274            }
 01275        }
 1276
 1277        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1278        {
 1279            IEnumerable<Task> GetTasks()
 1280            {
 1281                var messageId = Guid.NewGuid();
 1282                foreach (var session in sessions)
 1283                {
 1284                    var controllers = session.SessionControllers;
 1285                    foreach (var controller in controllers)
 1286                    {
 1287                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1288                    }
 1289                }
 1290            }
 1291
 211292            return Task.WhenAll(GetTasks());
 1293        }
 1294
 1295        /// <inheritdoc />
 1296        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1297        {
 01298            CheckDisposed();
 1299
 01300            var session = GetSessionToRemoteControl(sessionId);
 1301
 01302            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1303
 1304            List<BaseItem> items;
 1305
 01306            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1307            {
 01308                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01309                    .ToList();
 1310
 01311                command.PlayCommand = PlayCommand.PlayNow;
 1312            }
 1313            else
 1314            {
 01315                var list = new List<BaseItem>();
 01316                foreach (var itemId in command.ItemIds)
 1317                {
 01318                    var subItems = TranslateItemForPlayback(itemId, user);
 01319                    list.AddRange(subItems);
 1320                }
 1321
 01322                items = list;
 1323            }
 1324
 01325            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1326            {
 01327                items.Shuffle();
 01328                command.PlayCommand = PlayCommand.PlayNow;
 1329            }
 1330
 01331            command.ItemIds = items.Select(i => i.Id).ToArray();
 1332
 01333            if (user is not null)
 1334            {
 01335                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1336                {
 01337                    throw new ArgumentException(
 01338                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1339                }
 1340            }
 1341
 01342            if (user is not null
 01343                && command.ItemIds.Length == 1
 01344                && user.EnableNextEpisodeAutoPlay
 01345                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1346            {
 01347                var series = episode.Series;
 01348                if (series is not null)
 1349                {
 01350                    var episodes = series.GetEpisodes(
 01351                            user,
 01352                            new DtoOptions(false)
 01353                            {
 01354                                EnableImages = false
 01355                            },
 01356                            user.DisplayMissingEpisodes)
 01357                        .Where(i => !i.IsVirtualItem)
 01358                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01359                        .ToList();
 1360
 01361                    if (episodes.Count > 0)
 1362                    {
 01363                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1364                    }
 1365                }
 1366            }
 1367
 01368            if (!string.IsNullOrEmpty(controllingSessionId))
 1369            {
 01370                var controllingSession = GetSession(controllingSessionId);
 01371                AssertCanControl(session, controllingSession);
 01372                if (!controllingSession.UserId.IsEmpty())
 1373                {
 01374                    command.ControllingUserId = controllingSession.UserId;
 1375                }
 1376            }
 1377
 01378            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 01379        }
 1380
 1381        /// <inheritdoc />
 1382        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1383        {
 01384            CheckDisposed();
 01385            var session = GetSession(sessionId);
 01386            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 01387        }
 1388
 1389        /// <inheritdoc />
 1390        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1391        {
 01392            CheckDisposed();
 01393            var session = GetSession(sessionId);
 01394            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 01395        }
 1396
 1397        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1398        {
 01399            var item = _libraryManager.GetItemById(id);
 1400
 01401            if (item is null)
 1402            {
 01403                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01404                return Array.Empty<BaseItem>();
 1405            }
 1406
 01407            if (item is IItemByName byName)
 1408            {
 01409                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01410                {
 01411                    IsFolder = false,
 01412                    Recursive = true,
 01413                    DtoOptions = new DtoOptions(false)
 01414                    {
 01415                        EnableImages = false,
 01416                        Fields = new[]
 01417                        {
 01418                            ItemFields.SortName
 01419                        }
 01420                    },
 01421                    IsVirtualItem = false,
 01422                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01423                });
 1424            }
 1425
 01426            if (item.IsFolder)
 1427            {
 01428                var folder = (Folder)item;
 1429
 01430                return folder.GetItemList(new InternalItemsQuery(user)
 01431                {
 01432                    Recursive = true,
 01433                    IsFolder = false,
 01434                    DtoOptions = new DtoOptions(false)
 01435                    {
 01436                        EnableImages = false,
 01437                        Fields = new ItemFields[]
 01438                        {
 01439                            ItemFields.SortName
 01440                        }
 01441                    },
 01442                    IsVirtualItem = false,
 01443                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01444                });
 1445            }
 1446
 01447            return new[] { item };
 1448        }
 1449
 1450        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1451        {
 01452            var item = _libraryManager.GetItemById(id);
 1453
 01454            if (item is null)
 1455            {
 01456                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01457                return new List<BaseItem>();
 1458            }
 1459
 01460            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1461        }
 1462
 1463        /// <inheritdoc />
 1464        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1465        {
 01466            var generalCommand = new GeneralCommand
 01467            {
 01468                Name = GeneralCommandType.DisplayContent,
 01469                Arguments =
 01470                {
 01471                    ["ItemId"] = command.ItemId,
 01472                    ["ItemName"] = command.ItemName,
 01473                    ["ItemType"] = command.ItemType.ToString()
 01474                }
 01475            };
 1476
 01477            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1478        }
 1479
 1480        /// <inheritdoc />
 1481        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1482        {
 01483            CheckDisposed();
 1484
 01485            var session = GetSessionToRemoteControl(sessionId);
 1486
 01487            if (!string.IsNullOrEmpty(controllingSessionId))
 1488            {
 01489                var controllingSession = GetSession(controllingSessionId);
 01490                AssertCanControl(session, controllingSession);
 01491                if (!controllingSession.UserId.IsEmpty())
 1492                {
 01493                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1494                }
 1495            }
 1496
 01497            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1498        }
 1499
 1500        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1501        {
 01502            ArgumentNullException.ThrowIfNull(session);
 1503
 01504            ArgumentNullException.ThrowIfNull(controllingSession);
 01505        }
 1506
 1507        /// <summary>
 1508        /// Sends the restart required message.
 1509        /// </summary>
 1510        /// <param name="cancellationToken">The cancellation token.</param>
 1511        /// <returns>Task.</returns>
 1512        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1513        {
 01514            CheckDisposed();
 1515
 01516            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1517        }
 1518
 1519        /// <summary>
 1520        /// Adds the additional user.
 1521        /// </summary>
 1522        /// <param name="sessionId">The session identifier.</param>
 1523        /// <param name="userId">The user identifier.</param>
 1524        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1525        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1526        public void AddAdditionalUser(string sessionId, Guid userId)
 1527        {
 01528            CheckDisposed();
 1529
 01530            var session = GetSession(sessionId);
 1531
 01532            if (session.UserId.Equals(userId))
 1533            {
 01534                throw new ArgumentException("The requested user is already the primary user of the session.");
 1535            }
 1536
 01537            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1538            {
 01539                var user = _userManager.GetUserById(userId);
 01540                var newUser = new SessionUserInfo
 01541                {
 01542                    UserId = userId,
 01543                    UserName = user.Username
 01544                };
 1545
 01546                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1547            }
 01548        }
 1549
 1550        /// <summary>
 1551        /// Removes the additional user.
 1552        /// </summary>
 1553        /// <param name="sessionId">The session identifier.</param>
 1554        /// <param name="userId">The user identifier.</param>
 1555        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1556        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1557        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1558        {
 01559            CheckDisposed();
 1560
 01561            var session = GetSession(sessionId);
 1562
 01563            if (session.UserId.Equals(userId))
 1564            {
 01565                throw new ArgumentException("The requested user is already the primary user of the session.");
 1566            }
 1567
 01568            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1569
 01570            if (user is not null)
 1571            {
 01572                var list = session.AdditionalUsers.ToList();
 01573                list.Remove(user);
 1574
 01575                session.AdditionalUsers = list.ToArray();
 1576            }
 01577        }
 1578
 1579        /// <summary>
 1580        /// Authenticates the new session.
 1581        /// </summary>
 1582        /// <param name="request">The authenticationrequest.</param>
 1583        /// <returns>The authentication result.</returns>
 1584        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1585        {
 151586            return AuthenticateNewSessionInternal(request, true);
 1587        }
 1588
 1589        /// <summary>
 1590        /// Directly authenticates the session without enforcing password.
 1591        /// </summary>
 1592        /// <param name="request">The authentication request.</param>
 1593        /// <returns>The authentication result.</returns>
 1594        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1595        {
 01596            return AuthenticateNewSessionInternal(request, false);
 1597        }
 1598
 1599        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1600        {
 231601            CheckDisposed();
 1602
 231603            ArgumentException.ThrowIfNullOrEmpty(request.App);
 211604            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 191605            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 171606            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1607
 151608            User user = null;
 151609            if (!request.UserId.IsEmpty())
 1610            {
 01611                user = _userManager.GetUserById(request.UserId);
 1612            }
 1613
 151614            user ??= _userManager.GetUserByName(request.Username);
 1615
 151616            if (enforcePassword)
 1617            {
 151618                user = await _userManager.AuthenticateUser(
 151619                    request.Username,
 151620                    request.Password,
 151621                    request.RemoteEndPoint,
 151622                    true).ConfigureAwait(false);
 1623            }
 1624
 151625            if (user is null)
 1626            {
 01627                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01628                throw new AuthenticationException("Invalid username or password entered.");
 1629            }
 1630
 151631            if (!string.IsNullOrEmpty(request.DeviceId)
 151632                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1633            {
 01634                throw new SecurityException("User is not allowed access from this device.");
 1635            }
 1636
 151637            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 151638            int maxActiveSessions = user.MaxActiveSessions;
 151639            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 151640            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1641            {
 01642                throw new SecurityException("User is at their maximum number of sessions.");
 1643            }
 1644
 151645            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1646
 151647            var session = await LogSessionActivity(
 151648                request.App,
 151649                request.AppVersion,
 151650                request.DeviceId,
 151651                request.DeviceName,
 151652                request.RemoteEndPoint,
 151653                user).ConfigureAwait(false);
 1654
 151655            var returnResult = new AuthenticationResult
 151656            {
 151657                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 151658                SessionInfo = ToSessionInfoDto(session),
 151659                AccessToken = token,
 151660                ServerId = _appHost.SystemId
 151661            };
 1662
 151663            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 151664            return returnResult;
 151665        }
 1666
 1667        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1668        {
 1669            // This should be validated above, but if it isn't don't delete all tokens.
 171670            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1671
 151672            var existing = _deviceManager.GetDevices(
 151673                new DeviceQuery
 151674                {
 151675                    DeviceId = deviceId,
 151676                    UserId = user.Id
 151677                }).Items;
 1678
 301679            foreach (var auth in existing)
 1680            {
 1681                try
 1682                {
 1683                    // Logout any existing sessions for the user on this device
 01684                    await Logout(auth).ConfigureAwait(false);
 01685                }
 01686                catch (Exception ex)
 1687                {
 01688                    _logger.LogError(ex, "Error while logging out existing session.");
 01689                }
 1690            }
 1691
 151692            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 151693            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1694
 151695            return device.AccessToken;
 151696        }
 1697
 1698        /// <inheritdoc />
 1699        public async Task Logout(string accessToken)
 1700        {
 01701            CheckDisposed();
 1702
 01703            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1704
 01705            var existing = _deviceManager.GetDevices(
 01706                new DeviceQuery
 01707                {
 01708                    Limit = 1,
 01709                    AccessToken = accessToken
 01710                }).Items;
 1711
 01712            if (existing.Count > 0)
 1713            {
 01714                await Logout(existing[0]).ConfigureAwait(false);
 1715            }
 01716        }
 1717
 1718        /// <inheritdoc />
 1719        public async Task Logout(Device device)
 1720        {
 01721            CheckDisposed();
 1722
 01723            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1724
 01725            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1726
 01727            var sessions = Sessions
 01728                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01729                .ToList();
 1730
 01731            foreach (var session in sessions)
 1732            {
 1733                try
 1734                {
 01735                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01736                }
 01737                catch (Exception ex)
 1738                {
 01739                    _logger.LogError(ex, "Error reporting session ended");
 01740                }
 1741            }
 01742        }
 1743
 1744        /// <inheritdoc />
 1745        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1746        {
 21747            CheckDisposed();
 1748
 21749            var existing = _deviceManager.GetDevices(new DeviceQuery
 21750            {
 21751                UserId = userId
 21752            });
 1753
 41754            foreach (var info in existing.Items)
 1755            {
 01756                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1757                {
 01758                    await Logout(info).ConfigureAwait(false);
 1759                }
 1760            }
 21761        }
 1762
 1763        /// <summary>
 1764        /// Reports the capabilities.
 1765        /// </summary>
 1766        /// <param name="sessionId">The session identifier.</param>
 1767        /// <param name="capabilities">The capabilities.</param>
 1768        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1769        {
 01770            CheckDisposed();
 1771
 01772            var session = GetSession(sessionId);
 1773
 01774            ReportCapabilities(session, capabilities, true);
 01775        }
 1776
 1777        private void ReportCapabilities(
 1778            SessionInfo session,
 1779            ClientCapabilities capabilities,
 1780            bool saveCapabilities)
 1781        {
 151782            session.Capabilities = capabilities;
 1783
 151784            if (saveCapabilities)
 1785            {
 01786                CapabilitiesChanged?.Invoke(
 01787                    this,
 01788                    new SessionEventArgs
 01789                    {
 01790                        SessionInfo = session
 01791                    });
 1792
 01793                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1794            }
 151795        }
 1796
 1797        /// <summary>
 1798        /// Converts a BaseItem to a BaseItemInfo.
 1799        /// </summary>
 1800        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1801        {
 01802            ArgumentNullException.ThrowIfNull(item);
 1803
 01804            var dtoOptions = _itemInfoDtoOptions;
 1805
 01806            if (_itemInfoDtoOptions is null)
 1807            {
 01808                dtoOptions = new DtoOptions
 01809                {
 01810                    AddProgramRecordingInfo = false
 01811                };
 1812
 01813                var fields = dtoOptions.Fields.ToList();
 1814
 01815                fields.Remove(ItemFields.CanDelete);
 01816                fields.Remove(ItemFields.CanDownload);
 01817                fields.Remove(ItemFields.ChildCount);
 01818                fields.Remove(ItemFields.CustomRating);
 01819                fields.Remove(ItemFields.DateLastMediaAdded);
 01820                fields.Remove(ItemFields.DateLastRefreshed);
 01821                fields.Remove(ItemFields.DateLastSaved);
 01822                fields.Remove(ItemFields.DisplayPreferencesId);
 01823                fields.Remove(ItemFields.Etag);
 01824                fields.Remove(ItemFields.ItemCounts);
 01825                fields.Remove(ItemFields.MediaSourceCount);
 01826                fields.Remove(ItemFields.MediaStreams);
 01827                fields.Remove(ItemFields.MediaSources);
 01828                fields.Remove(ItemFields.People);
 01829                fields.Remove(ItemFields.PlayAccess);
 01830                fields.Remove(ItemFields.People);
 01831                fields.Remove(ItemFields.ProductionLocations);
 01832                fields.Remove(ItemFields.RecursiveItemCount);
 01833                fields.Remove(ItemFields.RemoteTrailers);
 01834                fields.Remove(ItemFields.SeasonUserData);
 01835                fields.Remove(ItemFields.Settings);
 01836                fields.Remove(ItemFields.SortName);
 01837                fields.Remove(ItemFields.Tags);
 1838
 01839                dtoOptions.Fields = fields.ToArray();
 1840
 01841                _itemInfoDtoOptions = dtoOptions;
 1842            }
 1843
 01844            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1845
 01846            if (mediaSource is not null)
 1847            {
 01848                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1849            }
 1850
 01851            return info;
 1852        }
 1853
 1854        private string GetImageCacheTag(User user)
 1855        {
 1856            try
 1857            {
 01858                return _imageProcessor.GetImageCacheTag(user);
 1859            }
 01860            catch (Exception e)
 1861            {
 01862                _logger.LogError(e, "Error getting image information for profile image");
 01863                return null;
 1864            }
 01865        }
 1866
 1867        /// <inheritdoc />
 1868        public void ReportNowViewingItem(string sessionId, string itemId)
 1869        {
 01870            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1871
 01872            var item = _libraryManager.GetItemById(new Guid(itemId));
 01873            var session = GetSession(sessionId);
 1874
 01875            session.NowViewingItem = GetItemInfo(item, null);
 01876        }
 1877
 1878        /// <inheritdoc />
 1879        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1880        {
 01881            var session = Sessions.FirstOrDefault(i =>
 01882                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1883
 01884            if (session is not null)
 1885            {
 01886                session.TranscodingInfo = info;
 1887            }
 01888        }
 1889
 1890        /// <inheritdoc />
 1891        public void ClearTranscodingInfo(string deviceId)
 1892        {
 01893            ReportTranscodingInfo(deviceId, null);
 01894        }
 1895
 1896        /// <inheritdoc />
 1897        public SessionInfo GetSession(string deviceId, string client, string version)
 1898        {
 01899            return Sessions.FirstOrDefault(i =>
 01900                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01901                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1902        }
 1903
 1904        /// <inheritdoc />
 1905        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1906        {
 01907            ArgumentNullException.ThrowIfNull(info);
 1908
 01909            var user = info.UserId.IsEmpty()
 01910                ? null
 01911                : _userManager.GetUserById(info.UserId);
 1912
 01913            appVersion = string.IsNullOrEmpty(appVersion)
 01914                ? info.AppVersion
 01915                : appVersion;
 1916
 01917            var deviceName = info.DeviceName;
 01918            var appName = info.AppName;
 1919
 01920            if (string.IsNullOrEmpty(deviceId))
 1921            {
 01922                deviceId = info.DeviceId;
 1923            }
 1924
 1925            // Prevent argument exception
 01926            if (string.IsNullOrEmpty(appVersion))
 1927            {
 01928                appVersion = "1";
 1929            }
 1930
 01931            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1932        }
 1933
 1934        /// <inheritdoc />
 1935        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1936        {
 01937            var items = _deviceManager.GetDevices(new DeviceQuery
 01938            {
 01939                AccessToken = token,
 01940                Limit = 1
 01941            }).Items;
 1942
 01943            if (items.Count == 0)
 1944            {
 01945                return null;
 1946            }
 1947
 01948            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 01949        }
 1950
 1951        /// <inheritdoc/>
 1952        public IReadOnlyList<SessionInfoDto> GetSessions(
 1953            Guid userId,
 1954            string deviceId,
 1955            int? activeWithinSeconds,
 1956            Guid? controllableUserToCheck,
 1957            bool isApiKey)
 1958        {
 01959            var result = Sessions;
 01960            if (!string.IsNullOrEmpty(deviceId))
 1961            {
 01962                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1963            }
 1964
 01965            var userCanControlOthers = false;
 01966            var userIsAdmin = false;
 01967            User user = null;
 1968
 01969            if (isApiKey)
 1970            {
 01971                userCanControlOthers = true;
 01972                userIsAdmin = true;
 1973            }
 01974            else if (!userId.IsEmpty())
 1975            {
 01976                user = _userManager.GetUserById(userId);
 01977                if (user is not null)
 1978                {
 01979                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01980                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1981                }
 1982                else
 1983                {
 01984                    return [];
 1985                }
 1986            }
 1987
 01988            if (!controllableUserToCheck.IsNullOrEmpty())
 1989            {
 01990                result = result.Where(i => i.SupportsRemoteControl);
 1991
 01992                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01993                if (controlledUser is null)
 1994                {
 01995                    return [];
 1996                }
 1997
 01998                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1999                {
 2000                    // Controlled user has device sharing disabled
 02001                    result = result.Where(i => !i.UserId.IsEmpty());
 2002                }
 2003
 02004                if (!userCanControlOthers)
 2005                {
 2006                    // User cannot control other user's sessions, validate user id.
 02007                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2008                }
 2009
 02010                result = result.Where(i =>
 02011                {
 02012                    if (isApiKey)
 02013                    {
 02014                        return true;
 02015                    }
 02016
 02017                    if (user is null)
 02018                    {
 02019                        return false;
 02020                    }
 02021
 02022                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02023                });
 2024            }
 02025            else if (!userIsAdmin)
 2026            {
 2027                // Request isn't from administrator, limit to "own" sessions.
 02028                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2029            }
 2030
 02031            if (!userIsAdmin)
 2032            {
 2033                // Don't report acceleration type for non-admin users.
 02034                result = result.Select(r =>
 02035                {
 02036                    if (r.TranscodingInfo is not null)
 02037                    {
 02038                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02039                    }
 02040
 02041                    return r;
 02042                });
 2043            }
 2044
 02045            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2046            {
 02047                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02048                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2049            }
 2050
 02051            return result.Select(ToSessionInfoDto).ToList();
 2052        }
 2053
 2054        /// <inheritdoc />
 2055        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2056        {
 02057            CheckDisposed();
 2058
 02059            var adminUserIds = _userManager.GetUsers()
 02060                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02061                .Select(i => i.Id)
 02062                .ToList();
 2063
 02064            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2065        }
 2066
 2067        /// <inheritdoc />
 2068        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2069        {
 02070            CheckDisposed();
 2071
 02072            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2073
 02074            if (sessions.Count == 0)
 2075            {
 02076                return Task.CompletedTask;
 2077            }
 2078
 02079            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2080        }
 2081
 2082        /// <inheritdoc />
 2083        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2084        {
 02085            CheckDisposed();
 2086
 02087            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 02088            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2089        }
 2090
 2091        /// <inheritdoc />
 2092        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2093        {
 02094            CheckDisposed();
 2095
 02096            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2097
 02098            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2099        }
 2100
 2101        /// <inheritdoc />
 2102        public async ValueTask DisposeAsync()
 2103        {
 312104            if (_disposed)
 2105            {
 02106                return;
 2107            }
 2108
 622109            foreach (var session in _activeConnections.Values)
 2110            {
 02111                await session.DisposeAsync().ConfigureAwait(false);
 2112            }
 2113
 312114            if (_idleTimer is not null)
 2115            {
 02116                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02117                _idleTimer = null;
 2118            }
 2119
 312120            if (_inactiveTimer is not null)
 2121            {
 02122                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02123                _inactiveTimer = null;
 2124            }
 2125
 312126            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2127
 312128            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 312129            _disposed = true;
 312130        }
 2131
 2132        private async void OnApplicationStopping()
 2133        {
 212134            _logger.LogInformation("Sending shutdown notifications");
 2135            try
 2136            {
 212137                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2138
 212139                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 212140            }
 02141            catch (Exception ex)
 2142            {
 02143                _logger.LogError(ex, "Error sending server shutdown notifications");
 02144            }
 2145
 2146            // Close open websockets to allow Kestrel to shut down cleanly
 722147            foreach (var session in _activeConnections.Values)
 2148            {
 152149                await session.DisposeAsync().ConfigureAwait(false);
 2150            }
 2151
 212152            _activeConnections.Clear();
 212153            _activeLiveStreamSessions.Clear();
 212154        }
 2155    }
 2156}

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()