< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Session.SessionManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Session/SessionManager.cs
Line coverage
21%
Covered lines: 207
Uncovered lines: 778
Coverable lines: 985
Total lines: 2149
Line coverage: 21%
Branch coverage
12%
Covered branches: 51
Total branches: 394
Branch coverage: 12.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21421/29/2026 - 12:13:32 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: 2149 1/23/2026 - 12:11:06 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21421/29/2026 - 12:13:32 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: 2149

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(...)100%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(...)62.5%161694.44%
CreateSessionInfo(...)57.14%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%1056320%
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 e)
 275                    {
 0276                        _logger.LogDebug(e, "Error updating user's last activity date.");
 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            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1025            {
 01026                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1027            }
 1028
 01029            var session = GetSession(info.SessionId);
 1030
 01031            session.StopAutomaticProgress();
 1032
 01033            var libraryItem = info.ItemId.IsEmpty()
 01034                ? null
 01035                : GetNowPlayingItem(session, info.ItemId);
 1036
 1037            // Normalize
 01038            if (string.IsNullOrEmpty(info.MediaSourceId))
 1039            {
 01040                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1041            }
 1042
 01043            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1044            {
 01045                var current = session.NowPlayingItem;
 1046
 01047                if (current is null || !info.ItemId.Equals(current.Id))
 1048                {
 01049                    MediaSourceInfo mediaSource = null;
 1050
 01051                    if (libraryItem is IHasMediaSources)
 1052                    {
 01053                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1054                    }
 1055
 01056                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1057                }
 1058                else
 1059                {
 01060                    info.Item = current;
 1061                }
 1062            }
 1063
 01064            if (info.Item is not null)
 1065            {
 01066                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1067
 01068                _logger.LogInformation(
 01069                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 01070                    session.UserName,
 01071                    info.Item.Name,
 01072                    msString,
 01073                    session.Client,
 01074                    session.ApplicationVersion);
 1075            }
 1076
 01077            if (info.NowPlayingQueue is not null)
 1078            {
 01079                session.NowPlayingQueue = info.NowPlayingQueue;
 1080            }
 1081
 01082            session.PlaylistItemId = info.PlaylistItemId;
 1083
 01084            RemoveNowPlayingItem(session);
 1085
 01086            var users = GetUsers(session);
 01087            var playedToCompletion = false;
 1088
 01089            if (libraryItem is not null)
 1090            {
 01091                foreach (var user in users)
 1092                {
 01093                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1094                }
 1095            }
 1096
 01097            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1098            {
 01099                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1100            }
 1101
 01102            var eventArgs = new PlaybackStopEventArgs
 01103            {
 01104                Item = libraryItem,
 01105                Users = users,
 01106                PlaybackPositionTicks = info.PositionTicks,
 01107                PlayedToCompletion = playedToCompletion,
 01108                MediaSourceId = info.MediaSourceId,
 01109                MediaInfo = info.Item,
 01110                DeviceName = session.DeviceName,
 01111                ClientName = session.Client,
 01112                DeviceId = session.DeviceId,
 01113                Session = session,
 01114                PlaySessionId = info.PlaySessionId
 01115            };
 1116
 01117            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1118
 01119            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 01120        }
 1121
 1122        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1123        {
 01124            if (playbackFailed)
 1125            {
 01126                return false;
 1127            }
 1128
 01129            var data = _userDataManager.GetUserData(user, item);
 1130            bool playedToCompletion;
 01131            if (positionTicks.HasValue)
 1132            {
 01133                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1134            }
 1135            else
 1136            {
 1137                // If the client isn't able to report this, then we'll just have to make an assumption
 01138                data.PlayCount++;
 01139                data.Played = item.SupportsPlayedStatus;
 01140                data.PlaybackPositionTicks = 0;
 01141                playedToCompletion = true;
 1142            }
 1143
 01144            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1145
 01146            return playedToCompletion;
 1147        }
 1148
 1149        /// <summary>
 1150        /// Gets the session.
 1151        /// </summary>
 1152        /// <param name="sessionId">The session identifier.</param>
 1153        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1154        /// <returns>SessionInfo.</returns>
 1155        /// <exception cref="ResourceNotFoundException">
 1156        /// No session with an Id equal to <c>sessionId</c> was found
 1157        /// and <c>throwOnMissing</c> is <c>true</c>.
 1158        /// </exception>
 1159        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1160        {
 01161            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01162            if (session is null && throwOnMissing)
 1163            {
 01164                throw new ResourceNotFoundException(
 01165                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1166            }
 1167
 01168            return session;
 1169        }
 1170
 1171        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1172        {
 1173            // Accept either device id or session id
 01174            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1175
 01176            if (session is null)
 1177            {
 01178                throw new ResourceNotFoundException(
 01179                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1180            }
 1181
 01182            return session;
 1183        }
 1184
 1185        /// <inheritdoc />
 1186        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1187        {
 151188            return new SessionInfoDto
 151189            {
 151190                PlayState = sessionInfo.PlayState,
 151191                AdditionalUsers = sessionInfo.AdditionalUsers,
 151192                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151193                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151194                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151195                Id = sessionInfo.Id,
 151196                UserId = sessionInfo.UserId,
 151197                UserName = sessionInfo.UserName,
 151198                Client = sessionInfo.Client,
 151199                LastActivityDate = sessionInfo.LastActivityDate,
 151200                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151201                LastPausedDate = sessionInfo.LastPausedDate,
 151202                DeviceName = sessionInfo.DeviceName,
 151203                DeviceType = sessionInfo.DeviceType,
 151204                NowPlayingItem = sessionInfo.NowPlayingItem,
 151205                NowViewingItem = sessionInfo.NowViewingItem,
 151206                DeviceId = sessionInfo.DeviceId,
 151207                ApplicationVersion = sessionInfo.ApplicationVersion,
 151208                TranscodingInfo = sessionInfo.TranscodingInfo,
 151209                IsActive = sessionInfo.IsActive,
 151210                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151211                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151212                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151213                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
 151214                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151215                PlaylistItemId = sessionInfo.PlaylistItemId,
 151216                ServerId = sessionInfo.ServerId,
 151217                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151218                SupportedCommands = sessionInfo.SupportedCommands
 151219            };
 1220        }
 1221
 1222        /// <inheritdoc />
 1223        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1224        {
 01225            CheckDisposed();
 1226
 01227            var generalCommand = new GeneralCommand
 01228            {
 01229                Name = GeneralCommandType.DisplayMessage
 01230            };
 1231
 01232            generalCommand.Arguments["Header"] = command.Header;
 01233            generalCommand.Arguments["Text"] = command.Text;
 1234
 01235            if (command.TimeoutMs.HasValue)
 1236            {
 01237                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1238            }
 1239
 01240            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1241        }
 1242
 1243        /// <inheritdoc />
 1244        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1245        {
 01246            CheckDisposed();
 1247
 01248            var session = GetSessionToRemoteControl(sessionId);
 1249
 01250            if (!string.IsNullOrEmpty(controllingSessionId))
 1251            {
 01252                var controllingSession = GetSession(controllingSessionId);
 01253                AssertCanControl(session, controllingSession);
 1254            }
 1255
 01256            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1257        }
 1258
 1259        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1260        {
 01261            var controllers = session.SessionControllers;
 01262            var messageId = Guid.NewGuid();
 1263
 01264            foreach (var controller in controllers)
 1265            {
 01266                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1267            }
 01268        }
 1269
 1270        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1271        {
 1272            IEnumerable<Task> GetTasks()
 1273            {
 1274                var messageId = Guid.NewGuid();
 1275                foreach (var session in sessions)
 1276                {
 1277                    var controllers = session.SessionControllers;
 1278                    foreach (var controller in controllers)
 1279                    {
 1280                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1281                    }
 1282                }
 1283            }
 1284
 211285            return Task.WhenAll(GetTasks());
 1286        }
 1287
 1288        /// <inheritdoc />
 1289        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1290        {
 01291            CheckDisposed();
 1292
 01293            var session = GetSessionToRemoteControl(sessionId);
 1294
 01295            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1296
 1297            List<BaseItem> items;
 1298
 01299            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1300            {
 01301                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01302                    .ToList();
 1303
 01304                command.PlayCommand = PlayCommand.PlayNow;
 1305            }
 1306            else
 1307            {
 01308                var list = new List<BaseItem>();
 01309                foreach (var itemId in command.ItemIds)
 1310                {
 01311                    var subItems = TranslateItemForPlayback(itemId, user);
 01312                    list.AddRange(subItems);
 1313                }
 1314
 01315                items = list;
 1316            }
 1317
 01318            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1319            {
 01320                items.Shuffle();
 01321                command.PlayCommand = PlayCommand.PlayNow;
 1322            }
 1323
 01324            command.ItemIds = items.Select(i => i.Id).ToArray();
 1325
 01326            if (user is not null)
 1327            {
 01328                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1329                {
 01330                    throw new ArgumentException(
 01331                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1332                }
 1333            }
 1334
 01335            if (user is not null
 01336                && command.ItemIds.Length == 1
 01337                && user.EnableNextEpisodeAutoPlay
 01338                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1339            {
 01340                var series = episode.Series;
 01341                if (series is not null)
 1342                {
 01343                    var episodes = series.GetEpisodes(
 01344                            user,
 01345                            new DtoOptions(false)
 01346                            {
 01347                                EnableImages = false
 01348                            },
 01349                            user.DisplayMissingEpisodes)
 01350                        .Where(i => !i.IsVirtualItem)
 01351                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01352                        .ToList();
 1353
 01354                    if (episodes.Count > 0)
 1355                    {
 01356                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1357                    }
 1358                }
 1359            }
 1360
 01361            if (!string.IsNullOrEmpty(controllingSessionId))
 1362            {
 01363                var controllingSession = GetSession(controllingSessionId);
 01364                AssertCanControl(session, controllingSession);
 01365                if (!controllingSession.UserId.IsEmpty())
 1366                {
 01367                    command.ControllingUserId = controllingSession.UserId;
 1368                }
 1369            }
 1370
 01371            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 01372        }
 1373
 1374        /// <inheritdoc />
 1375        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1376        {
 01377            CheckDisposed();
 01378            var session = GetSession(sessionId);
 01379            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 01380        }
 1381
 1382        /// <inheritdoc />
 1383        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1384        {
 01385            CheckDisposed();
 01386            var session = GetSession(sessionId);
 01387            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 01388        }
 1389
 1390        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1391        {
 01392            var item = _libraryManager.GetItemById(id);
 1393
 01394            if (item is null)
 1395            {
 01396                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01397                return Array.Empty<BaseItem>();
 1398            }
 1399
 01400            if (item is IItemByName byName)
 1401            {
 01402                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01403                {
 01404                    IsFolder = false,
 01405                    Recursive = true,
 01406                    DtoOptions = new DtoOptions(false)
 01407                    {
 01408                        EnableImages = false,
 01409                        Fields = new[]
 01410                        {
 01411                            ItemFields.SortName
 01412                        }
 01413                    },
 01414                    IsVirtualItem = false,
 01415                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01416                });
 1417            }
 1418
 01419            if (item.IsFolder)
 1420            {
 01421                var folder = (Folder)item;
 1422
 01423                return folder.GetItemList(new InternalItemsQuery(user)
 01424                {
 01425                    Recursive = true,
 01426                    IsFolder = false,
 01427                    DtoOptions = new DtoOptions(false)
 01428                    {
 01429                        EnableImages = false,
 01430                        Fields = new ItemFields[]
 01431                        {
 01432                            ItemFields.SortName
 01433                        }
 01434                    },
 01435                    IsVirtualItem = false,
 01436                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01437                });
 1438            }
 1439
 01440            return new[] { item };
 1441        }
 1442
 1443        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1444        {
 01445            var item = _libraryManager.GetItemById(id);
 1446
 01447            if (item is null)
 1448            {
 01449                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01450                return new List<BaseItem>();
 1451            }
 1452
 01453            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1454        }
 1455
 1456        /// <inheritdoc />
 1457        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1458        {
 01459            var generalCommand = new GeneralCommand
 01460            {
 01461                Name = GeneralCommandType.DisplayContent,
 01462                Arguments =
 01463                {
 01464                    ["ItemId"] = command.ItemId,
 01465                    ["ItemName"] = command.ItemName,
 01466                    ["ItemType"] = command.ItemType.ToString()
 01467                }
 01468            };
 1469
 01470            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1471        }
 1472
 1473        /// <inheritdoc />
 1474        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1475        {
 01476            CheckDisposed();
 1477
 01478            var session = GetSessionToRemoteControl(sessionId);
 1479
 01480            if (!string.IsNullOrEmpty(controllingSessionId))
 1481            {
 01482                var controllingSession = GetSession(controllingSessionId);
 01483                AssertCanControl(session, controllingSession);
 01484                if (!controllingSession.UserId.IsEmpty())
 1485                {
 01486                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1487                }
 1488            }
 1489
 01490            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1491        }
 1492
 1493        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1494        {
 01495            ArgumentNullException.ThrowIfNull(session);
 1496
 01497            ArgumentNullException.ThrowIfNull(controllingSession);
 01498        }
 1499
 1500        /// <summary>
 1501        /// Sends the restart required message.
 1502        /// </summary>
 1503        /// <param name="cancellationToken">The cancellation token.</param>
 1504        /// <returns>Task.</returns>
 1505        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1506        {
 01507            CheckDisposed();
 1508
 01509            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1510        }
 1511
 1512        /// <summary>
 1513        /// Adds the additional user.
 1514        /// </summary>
 1515        /// <param name="sessionId">The session identifier.</param>
 1516        /// <param name="userId">The user identifier.</param>
 1517        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1518        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1519        public void AddAdditionalUser(string sessionId, Guid userId)
 1520        {
 01521            CheckDisposed();
 1522
 01523            var session = GetSession(sessionId);
 1524
 01525            if (session.UserId.Equals(userId))
 1526            {
 01527                throw new ArgumentException("The requested user is already the primary user of the session.");
 1528            }
 1529
 01530            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1531            {
 01532                var user = _userManager.GetUserById(userId);
 01533                var newUser = new SessionUserInfo
 01534                {
 01535                    UserId = userId,
 01536                    UserName = user.Username
 01537                };
 1538
 01539                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1540            }
 01541        }
 1542
 1543        /// <summary>
 1544        /// Removes the additional user.
 1545        /// </summary>
 1546        /// <param name="sessionId">The session identifier.</param>
 1547        /// <param name="userId">The user identifier.</param>
 1548        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1549        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1550        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1551        {
 01552            CheckDisposed();
 1553
 01554            var session = GetSession(sessionId);
 1555
 01556            if (session.UserId.Equals(userId))
 1557            {
 01558                throw new ArgumentException("The requested user is already the primary user of the session.");
 1559            }
 1560
 01561            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1562
 01563            if (user is not null)
 1564            {
 01565                var list = session.AdditionalUsers.ToList();
 01566                list.Remove(user);
 1567
 01568                session.AdditionalUsers = list.ToArray();
 1569            }
 01570        }
 1571
 1572        /// <summary>
 1573        /// Authenticates the new session.
 1574        /// </summary>
 1575        /// <param name="request">The authenticationrequest.</param>
 1576        /// <returns>The authentication result.</returns>
 1577        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1578        {
 151579            return AuthenticateNewSessionInternal(request, true);
 1580        }
 1581
 1582        /// <summary>
 1583        /// Directly authenticates the session without enforcing password.
 1584        /// </summary>
 1585        /// <param name="request">The authentication request.</param>
 1586        /// <returns>The authentication result.</returns>
 1587        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1588        {
 01589            return AuthenticateNewSessionInternal(request, false);
 1590        }
 1591
 1592        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1593        {
 231594            CheckDisposed();
 1595
 231596            ArgumentException.ThrowIfNullOrEmpty(request.App);
 211597            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 191598            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 171599            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1600
 151601            User user = null;
 151602            if (!request.UserId.IsEmpty())
 1603            {
 01604                user = _userManager.GetUserById(request.UserId);
 1605            }
 1606
 151607            user ??= _userManager.GetUserByName(request.Username);
 1608
 151609            if (enforcePassword)
 1610            {
 151611                user = await _userManager.AuthenticateUser(
 151612                    request.Username,
 151613                    request.Password,
 151614                    request.RemoteEndPoint,
 151615                    true).ConfigureAwait(false);
 1616            }
 1617
 151618            if (user is null)
 1619            {
 01620                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01621                throw new AuthenticationException("Invalid username or password entered.");
 1622            }
 1623
 151624            if (!string.IsNullOrEmpty(request.DeviceId)
 151625                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1626            {
 01627                throw new SecurityException("User is not allowed access from this device.");
 1628            }
 1629
 151630            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 151631            int maxActiveSessions = user.MaxActiveSessions;
 151632            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 151633            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1634            {
 01635                throw new SecurityException("User is at their maximum number of sessions.");
 1636            }
 1637
 151638            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1639
 151640            var session = await LogSessionActivity(
 151641                request.App,
 151642                request.AppVersion,
 151643                request.DeviceId,
 151644                request.DeviceName,
 151645                request.RemoteEndPoint,
 151646                user).ConfigureAwait(false);
 1647
 151648            var returnResult = new AuthenticationResult
 151649            {
 151650                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 151651                SessionInfo = ToSessionInfoDto(session),
 151652                AccessToken = token,
 151653                ServerId = _appHost.SystemId
 151654            };
 1655
 151656            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 151657            return returnResult;
 151658        }
 1659
 1660        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1661        {
 1662            // This should be validated above, but if it isn't don't delete all tokens.
 171663            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1664
 151665            var existing = _deviceManager.GetDevices(
 151666                new DeviceQuery
 151667                {
 151668                    DeviceId = deviceId,
 151669                    UserId = user.Id
 151670                }).Items;
 1671
 301672            foreach (var auth in existing)
 1673            {
 1674                try
 1675                {
 1676                    // Logout any existing sessions for the user on this device
 01677                    await Logout(auth).ConfigureAwait(false);
 01678                }
 01679                catch (Exception ex)
 1680                {
 01681                    _logger.LogError(ex, "Error while logging out existing session.");
 01682                }
 1683            }
 1684
 151685            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 151686            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1687
 151688            return device.AccessToken;
 151689        }
 1690
 1691        /// <inheritdoc />
 1692        public async Task Logout(string accessToken)
 1693        {
 01694            CheckDisposed();
 1695
 01696            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1697
 01698            var existing = _deviceManager.GetDevices(
 01699                new DeviceQuery
 01700                {
 01701                    Limit = 1,
 01702                    AccessToken = accessToken
 01703                }).Items;
 1704
 01705            if (existing.Count > 0)
 1706            {
 01707                await Logout(existing[0]).ConfigureAwait(false);
 1708            }
 01709        }
 1710
 1711        /// <inheritdoc />
 1712        public async Task Logout(Device device)
 1713        {
 01714            CheckDisposed();
 1715
 01716            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1717
 01718            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1719
 01720            var sessions = Sessions
 01721                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01722                .ToList();
 1723
 01724            foreach (var session in sessions)
 1725            {
 1726                try
 1727                {
 01728                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01729                }
 01730                catch (Exception ex)
 1731                {
 01732                    _logger.LogError(ex, "Error reporting session ended");
 01733                }
 1734            }
 01735        }
 1736
 1737        /// <inheritdoc />
 1738        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1739        {
 21740            CheckDisposed();
 1741
 21742            var existing = _deviceManager.GetDevices(new DeviceQuery
 21743            {
 21744                UserId = userId
 21745            });
 1746
 41747            foreach (var info in existing.Items)
 1748            {
 01749                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1750                {
 01751                    await Logout(info).ConfigureAwait(false);
 1752                }
 1753            }
 21754        }
 1755
 1756        /// <summary>
 1757        /// Reports the capabilities.
 1758        /// </summary>
 1759        /// <param name="sessionId">The session identifier.</param>
 1760        /// <param name="capabilities">The capabilities.</param>
 1761        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1762        {
 01763            CheckDisposed();
 1764
 01765            var session = GetSession(sessionId);
 1766
 01767            ReportCapabilities(session, capabilities, true);
 01768        }
 1769
 1770        private void ReportCapabilities(
 1771            SessionInfo session,
 1772            ClientCapabilities capabilities,
 1773            bool saveCapabilities)
 1774        {
 151775            session.Capabilities = capabilities;
 1776
 151777            if (saveCapabilities)
 1778            {
 01779                CapabilitiesChanged?.Invoke(
 01780                    this,
 01781                    new SessionEventArgs
 01782                    {
 01783                        SessionInfo = session
 01784                    });
 1785
 01786                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1787            }
 151788        }
 1789
 1790        /// <summary>
 1791        /// Converts a BaseItem to a BaseItemInfo.
 1792        /// </summary>
 1793        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1794        {
 01795            ArgumentNullException.ThrowIfNull(item);
 1796
 01797            var dtoOptions = _itemInfoDtoOptions;
 1798
 01799            if (_itemInfoDtoOptions is null)
 1800            {
 01801                dtoOptions = new DtoOptions
 01802                {
 01803                    AddProgramRecordingInfo = false
 01804                };
 1805
 01806                var fields = dtoOptions.Fields.ToList();
 1807
 01808                fields.Remove(ItemFields.CanDelete);
 01809                fields.Remove(ItemFields.CanDownload);
 01810                fields.Remove(ItemFields.ChildCount);
 01811                fields.Remove(ItemFields.CustomRating);
 01812                fields.Remove(ItemFields.DateLastMediaAdded);
 01813                fields.Remove(ItemFields.DateLastRefreshed);
 01814                fields.Remove(ItemFields.DateLastSaved);
 01815                fields.Remove(ItemFields.DisplayPreferencesId);
 01816                fields.Remove(ItemFields.Etag);
 01817                fields.Remove(ItemFields.ItemCounts);
 01818                fields.Remove(ItemFields.MediaSourceCount);
 01819                fields.Remove(ItemFields.MediaStreams);
 01820                fields.Remove(ItemFields.MediaSources);
 01821                fields.Remove(ItemFields.People);
 01822                fields.Remove(ItemFields.PlayAccess);
 01823                fields.Remove(ItemFields.People);
 01824                fields.Remove(ItemFields.ProductionLocations);
 01825                fields.Remove(ItemFields.RecursiveItemCount);
 01826                fields.Remove(ItemFields.RemoteTrailers);
 01827                fields.Remove(ItemFields.SeasonUserData);
 01828                fields.Remove(ItemFields.Settings);
 01829                fields.Remove(ItemFields.SortName);
 01830                fields.Remove(ItemFields.Tags);
 1831
 01832                dtoOptions.Fields = fields.ToArray();
 1833
 01834                _itemInfoDtoOptions = dtoOptions;
 1835            }
 1836
 01837            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1838
 01839            if (mediaSource is not null)
 1840            {
 01841                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1842            }
 1843
 01844            return info;
 1845        }
 1846
 1847        private string GetImageCacheTag(User user)
 1848        {
 1849            try
 1850            {
 01851                return _imageProcessor.GetImageCacheTag(user);
 1852            }
 01853            catch (Exception e)
 1854            {
 01855                _logger.LogError(e, "Error getting image information for profile image");
 01856                return null;
 1857            }
 01858        }
 1859
 1860        /// <inheritdoc />
 1861        public void ReportNowViewingItem(string sessionId, string itemId)
 1862        {
 01863            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1864
 01865            var item = _libraryManager.GetItemById(new Guid(itemId));
 01866            var session = GetSession(sessionId);
 1867
 01868            session.NowViewingItem = GetItemInfo(item, null);
 01869        }
 1870
 1871        /// <inheritdoc />
 1872        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1873        {
 01874            var session = Sessions.FirstOrDefault(i =>
 01875                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1876
 01877            if (session is not null)
 1878            {
 01879                session.TranscodingInfo = info;
 1880            }
 01881        }
 1882
 1883        /// <inheritdoc />
 1884        public void ClearTranscodingInfo(string deviceId)
 1885        {
 01886            ReportTranscodingInfo(deviceId, null);
 01887        }
 1888
 1889        /// <inheritdoc />
 1890        public SessionInfo GetSession(string deviceId, string client, string version)
 1891        {
 01892            return Sessions.FirstOrDefault(i =>
 01893                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01894                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1895        }
 1896
 1897        /// <inheritdoc />
 1898        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1899        {
 01900            ArgumentNullException.ThrowIfNull(info);
 1901
 01902            var user = info.UserId.IsEmpty()
 01903                ? null
 01904                : _userManager.GetUserById(info.UserId);
 1905
 01906            appVersion = string.IsNullOrEmpty(appVersion)
 01907                ? info.AppVersion
 01908                : appVersion;
 1909
 01910            var deviceName = info.DeviceName;
 01911            var appName = info.AppName;
 1912
 01913            if (string.IsNullOrEmpty(deviceId))
 1914            {
 01915                deviceId = info.DeviceId;
 1916            }
 1917
 1918            // Prevent argument exception
 01919            if (string.IsNullOrEmpty(appVersion))
 1920            {
 01921                appVersion = "1";
 1922            }
 1923
 01924            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1925        }
 1926
 1927        /// <inheritdoc />
 1928        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1929        {
 01930            var items = _deviceManager.GetDevices(new DeviceQuery
 01931            {
 01932                AccessToken = token,
 01933                Limit = 1
 01934            }).Items;
 1935
 01936            if (items.Count == 0)
 1937            {
 01938                return null;
 1939            }
 1940
 01941            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 01942        }
 1943
 1944        /// <inheritdoc/>
 1945        public IReadOnlyList<SessionInfoDto> GetSessions(
 1946            Guid userId,
 1947            string deviceId,
 1948            int? activeWithinSeconds,
 1949            Guid? controllableUserToCheck,
 1950            bool isApiKey)
 1951        {
 01952            var result = Sessions;
 01953            if (!string.IsNullOrEmpty(deviceId))
 1954            {
 01955                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1956            }
 1957
 01958            var userCanControlOthers = false;
 01959            var userIsAdmin = false;
 01960            User user = null;
 1961
 01962            if (isApiKey)
 1963            {
 01964                userCanControlOthers = true;
 01965                userIsAdmin = true;
 1966            }
 01967            else if (!userId.IsEmpty())
 1968            {
 01969                user = _userManager.GetUserById(userId);
 01970                if (user is not null)
 1971                {
 01972                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01973                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1974                }
 1975                else
 1976                {
 01977                    return [];
 1978                }
 1979            }
 1980
 01981            if (!controllableUserToCheck.IsNullOrEmpty())
 1982            {
 01983                result = result.Where(i => i.SupportsRemoteControl);
 1984
 01985                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01986                if (controlledUser is null)
 1987                {
 01988                    return [];
 1989                }
 1990
 01991                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1992                {
 1993                    // Controlled user has device sharing disabled
 01994                    result = result.Where(i => !i.UserId.IsEmpty());
 1995                }
 1996
 01997                if (!userCanControlOthers)
 1998                {
 1999                    // User cannot control other user's sessions, validate user id.
 02000                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2001                }
 2002
 02003                result = result.Where(i =>
 02004                {
 02005                    if (isApiKey)
 02006                    {
 02007                        return true;
 02008                    }
 02009
 02010                    if (user is null)
 02011                    {
 02012                        return false;
 02013                    }
 02014
 02015                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02016                });
 2017            }
 02018            else if (!userIsAdmin)
 2019            {
 2020                // Request isn't from administrator, limit to "own" sessions.
 02021                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2022            }
 2023
 02024            if (!userIsAdmin)
 2025            {
 2026                // Don't report acceleration type for non-admin users.
 02027                result = result.Select(r =>
 02028                {
 02029                    if (r.TranscodingInfo is not null)
 02030                    {
 02031                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02032                    }
 02033
 02034                    return r;
 02035                });
 2036            }
 2037
 02038            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2039            {
 02040                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02041                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2042            }
 2043
 02044            return result.Select(ToSessionInfoDto).ToList();
 2045        }
 2046
 2047        /// <inheritdoc />
 2048        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2049        {
 02050            CheckDisposed();
 2051
 02052            var adminUserIds = _userManager.Users
 02053                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02054                .Select(i => i.Id)
 02055                .ToList();
 2056
 02057            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2058        }
 2059
 2060        /// <inheritdoc />
 2061        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2062        {
 02063            CheckDisposed();
 2064
 02065            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2066
 02067            if (sessions.Count == 0)
 2068            {
 02069                return Task.CompletedTask;
 2070            }
 2071
 02072            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2073        }
 2074
 2075        /// <inheritdoc />
 2076        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2077        {
 02078            CheckDisposed();
 2079
 02080            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 02081            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2082        }
 2083
 2084        /// <inheritdoc />
 2085        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2086        {
 02087            CheckDisposed();
 2088
 02089            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2090
 02091            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2092        }
 2093
 2094        /// <inheritdoc />
 2095        public async ValueTask DisposeAsync()
 2096        {
 312097            if (_disposed)
 2098            {
 02099                return;
 2100            }
 2101
 622102            foreach (var session in _activeConnections.Values)
 2103            {
 02104                await session.DisposeAsync().ConfigureAwait(false);
 2105            }
 2106
 312107            if (_idleTimer is not null)
 2108            {
 02109                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02110                _idleTimer = null;
 2111            }
 2112
 312113            if (_inactiveTimer is not null)
 2114            {
 02115                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02116                _inactiveTimer = null;
 2117            }
 2118
 312119            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2120
 312121            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 312122            _disposed = true;
 312123        }
 2124
 2125        private async void OnApplicationStopping()
 2126        {
 212127            _logger.LogInformation("Sending shutdown notifications");
 2128            try
 2129            {
 212130                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2131
 212132                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 212133            }
 02134            catch (Exception ex)
 2135            {
 02136                _logger.LogError(ex, "Error sending server shutdown notifications");
 02137            }
 2138
 2139            // Close open websockets to allow Kestrel to shut down cleanly
 722140            foreach (var session in _activeConnections.Values)
 2141            {
 152142                await session.DisposeAsync().ConfigureAwait(false);
 2143            }
 2144
 212145            _activeConnections.Clear();
 212146            _activeLiveStreamSessions.Clear();
 212147        }
 2148    }
 2149}

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