< 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
25%
Covered lines: 256
Uncovered lines: 742
Coverable lines: 998
Total lines: 2197
Line coverage: 25.6%
Branch coverage
13%
Covered branches: 57
Total branches: 416
Branch coverage: 13.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

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

Coverage delta

Coverage delta 5 -5

Metrics

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

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Linq;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Queries;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Database.Implementations.Entities.Security;
 16using Jellyfin.Database.Implementations.Enums;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Events;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Devices;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Dto;
 26using MediaBrowser.Controller.Entities;
 27using MediaBrowser.Controller.Events;
 28using MediaBrowser.Controller.Events.Authentication;
 29using MediaBrowser.Controller.Events.Session;
 30using MediaBrowser.Controller.Library;
 31using MediaBrowser.Controller.Net;
 32using MediaBrowser.Controller.Session;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.Querying;
 37using MediaBrowser.Model.Session;
 38using MediaBrowser.Model.SyncPlay;
 39using Microsoft.EntityFrameworkCore;
 40using Microsoft.Extensions.Hosting;
 41using Microsoft.Extensions.Logging;
 42using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 43
 44namespace Emby.Server.Implementations.Session
 45{
 46    /// <summary>
 47    /// Class SessionManager.
 48    /// </summary>
 49    public sealed class SessionManager : ISessionManager, IAsyncDisposable
 50    {
 51        private readonly IUserDataManager _userDataManager;
 52        private readonly IServerConfigurationManager _config;
 53        private readonly ILogger<SessionManager> _logger;
 54        private readonly IEventManager _eventManager;
 55        private readonly ILibraryManager _libraryManager;
 56        private readonly IUserManager _userManager;
 57        private readonly IMusicManager _musicManager;
 58        private readonly IDtoService _dtoService;
 59        private readonly IImageProcessor _imageProcessor;
 60        private readonly IMediaSourceManager _mediaSourceManager;
 61        private readonly IServerApplicationHost _appHost;
 62        private readonly IDeviceManager _deviceManager;
 63        private readonly CancellationTokenRegistration _shutdownCallback;
 3264        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 3265            = new(StringComparer.OrdinalIgnoreCase);
 66
 3267        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
 3268            = 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        {
 32107            _logger = logger;
 32108            _eventManager = eventManager;
 32109            _userDataManager = userDataManager;
 32110            _config = serverConfigurationManager;
 32111            _libraryManager = libraryManager;
 32112            _userManager = userManager;
 32113            _musicManager = musicManager;
 32114            _dtoService = dtoService;
 32115            _imageProcessor = imageProcessor;
 32116            _appHost = appHost;
 32117            _deviceManager = deviceManager;
 32118            _mediaSourceManager = mediaSourceManager;
 32119            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 120
 32121            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 32122        }
 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>
 43158        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        {
 111181            ObjectDisposedException.ThrowIf(_disposed, this);
 111182        }
 183
 184        private void OnSessionStarted(SessionInfo info)
 185        {
 17186            if (!string.IsNullOrEmpty(info.DeviceId))
 187            {
 17188                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 189
 17190                if (capabilities is not null)
 191                {
 17192                    ReportCapabilities(info, capabilities, false);
 193                }
 194            }
 195
 17196            _eventManager.Publish(new SessionStartedEventArgs(info));
 197
 17198            EventHelper.QueueEventIfNotNull(
 17199                SessionStarted,
 17200                this,
 17201                new SessionEventArgs
 17202                {
 17203                    SessionInfo = info
 17204                },
 17205                _logger);
 17206        }
 207
 208        private async ValueTask OnSessionEnded(SessionInfo info)
 209        {
 1210            EventHelper.QueueEventIfNotNull(
 1211                SessionEnded,
 1212                this,
 1213                new SessionEventArgs
 1214                {
 1215                    SessionInfo = info
 1216                },
 1217                _logger);
 218
 1219            _eventManager.Publish(new SessionEndedEventArgs(info));
 220
 1221            await info.DisposeAsync().ConfigureAwait(false);
 1222        }
 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        {
 40252            CheckDisposed();
 253
 40254            ArgumentException.ThrowIfNullOrEmpty(appName);
 40255            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 40256            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 257
 40258            var activityDate = DateTime.UtcNow;
 40259            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 40260            var lastActivityDate = session.LastActivityDate;
 40261            session.LastActivityDate = activityDate;
 262
 40263            if (user is not null)
 264            {
 40265                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 266
 40267                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 268                {
 269                    try
 270                    {
 16271                        user.LastActivityDate = activityDate;
 16272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 16273                    }
 0274                    catch (DbUpdateConcurrencyException)
 275                    {
 0276                        _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is 
 0277                    }
 278                }
 279            }
 280
 40281            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 282            {
 17283                SessionActivity?.Invoke(
 17284                    this,
 17285                    new SessionEventArgs
 17286                    {
 17287                        SessionInfo = session
 17288                    });
 289            }
 290
 40291            return session;
 40292        }
 293
 294        /// <inheritdoc />
 295        public void OnSessionControllerConnected(SessionInfo session)
 296        {
 1297            EventHelper.QueueEventIfNotNull(
 1298                SessionControllerConnected,
 1299                this,
 1300                new SessionEventArgs
 1301                {
 1302                    SessionInfo = session
 1303                },
 1304                _logger);
 1305        }
 306
 307        /// <inheritdoc />
 308        public async Task CloseIfNeededAsync(SessionInfo session)
 309        {
 1310            if (!session.SessionControllers.Any(i => i.IsSessionActive))
 311            {
 1312                var key = GetSessionKey(session.Client, session.DeviceId);
 313
 1314                _activeConnections.TryRemove(key, out _);
 1315                if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
 316                {
 0317                    await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false)
 318                }
 319
 1320                await OnSessionEnded(session).ConfigureAwait(false);
 321            }
 1322        }
 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            else
 347            {
 0348                liveStreamNeedsToBeClosed = true;
 349            }
 350
 0351            if (liveStreamNeedsToBeClosed)
 352            {
 353                try
 354                {
 0355                    await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
 0356                }
 0357                catch (Exception ex)
 358                {
 0359                    _logger.LogError(ex, "Error closing live stream");
 0360                }
 361            }
 0362        }
 363
 364        /// <inheritdoc />
 365        public async ValueTask ReportSessionEnded(string sessionId)
 366        {
 0367            CheckDisposed();
 0368            var session = GetSession(sessionId, false);
 369
 0370            if (session is not null)
 371            {
 0372                var key = GetSessionKey(session.Client, session.DeviceId);
 373
 0374                _activeConnections.TryRemove(key, out _);
 375
 0376                await OnSessionEnded(session).ConfigureAwait(false);
 377            }
 0378        }
 379
 380        private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
 381        {
 0382            return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
 383        }
 384
 385        /// <summary>
 386        /// Updates the now playing item id.
 387        /// </summary>
 388        /// <returns>Task.</returns>
 389        private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bo
 390        {
 0391            if (session is null)
 392            {
 0393                return;
 394            }
 395
 0396            if (string.IsNullOrEmpty(info.MediaSourceId))
 397            {
 0398                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 399            }
 400
 0401            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 402            {
 0403                var current = session.NowPlayingItem;
 404
 0405                if (current is null || !info.ItemId.Equals(current.Id))
 406                {
 0407                    var runtimeTicks = libraryItem.RunTimeTicks;
 408
 0409                    MediaSourceInfo mediaSource = null;
 0410                    if (libraryItem is IHasMediaSources)
 411                    {
 0412                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 413
 0414                        if (mediaSource is not null)
 415                        {
 0416                            runtimeTicks = mediaSource.RunTimeTicks;
 417                        }
 418                    }
 419
 0420                    info.Item = GetItemInfo(libraryItem, mediaSource);
 421
 0422                    info.Item.RunTimeTicks = runtimeTicks;
 423                }
 424                else
 425                {
 0426                    info.Item = current;
 427                }
 428            }
 429
 0430            session.NowPlayingItem = info.Item;
 0431            session.LastActivityDate = DateTime.UtcNow;
 432
 0433            if (updateLastCheckInTime)
 434            {
 0435                session.LastPlaybackCheckIn = DateTime.UtcNow;
 436            }
 437
 0438            if (info.IsPaused && session.LastPausedDate is null)
 439            {
 0440                session.LastPausedDate = DateTime.UtcNow;
 441            }
 0442            else if (!info.IsPaused)
 443            {
 0444                session.LastPausedDate = null;
 445            }
 446
 0447            session.PlayState.IsPaused = info.IsPaused;
 0448            session.PlayState.PositionTicks = info.PositionTicks;
 0449            session.PlayState.MediaSourceId = info.MediaSourceId;
 0450            session.PlayState.LiveStreamId = info.LiveStreamId;
 0451            session.PlayState.CanSeek = info.CanSeek;
 0452            session.PlayState.IsMuted = info.IsMuted;
 0453            session.PlayState.VolumeLevel = info.VolumeLevel;
 0454            session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
 0455            session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0456            session.PlayState.PlayMethod = info.PlayMethod;
 0457            session.PlayState.RepeatMode = info.RepeatMode;
 0458            session.PlayState.PlaybackOrder = info.PlaybackOrder;
 0459            session.PlaylistItemId = info.PlaylistItemId;
 0460        }
 461
 462        /// <summary>
 463        /// Removes the now playing item id.
 464        /// </summary>
 465        /// <param name="session">The session.</param>
 466        private void RemoveNowPlayingItem(SessionInfo session)
 467        {
 0468            session.NowPlayingItem = null;
 0469            session.FullNowPlayingItem = null;
 0470            session.PlayState = new PlayerStateInfo();
 471
 0472            if (!string.IsNullOrEmpty(session.DeviceId))
 473            {
 0474                ClearTranscodingInfo(session.DeviceId);
 475            }
 0476        }
 477
 478        private static string GetSessionKey(string appName, string deviceId)
 41479            => appName + deviceId;
 480
 481        /// <summary>
 482        /// Gets the connection.
 483        /// </summary>
 484        /// <param name="appName">Type of the client.</param>
 485        /// <param name="appVersion">The app version.</param>
 486        /// <param name="deviceId">The device id.</param>
 487        /// <param name="deviceName">Name of the device.</param>
 488        /// <param name="remoteEndPoint">The remote end point.</param>
 489        /// <param name="user">The user.</param>
 490        /// <returns>SessionInfo.</returns>
 491        private SessionInfo GetSessionInfo(
 492            string appName,
 493            string appVersion,
 494            string deviceId,
 495            string deviceName,
 496            string remoteEndPoint,
 497            User user)
 498        {
 40499            CheckDisposed();
 500
 40501            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 502
 40503            var key = GetSessionKey(appName, deviceId);
 40504            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 40505            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 40506            if (ReferenceEquals(newSession, sessionInfo))
 507            {
 17508                OnSessionStarted(newSession);
 509            }
 510
 40511            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 40512            sessionInfo.UserName = user?.Username;
 40513            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 40514            sessionInfo.RemoteEndPoint = remoteEndPoint;
 40515            sessionInfo.Client = appName;
 516
 40517            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 518            {
 40519                sessionInfo.DeviceName = deviceName;
 520            }
 521
 40522            sessionInfo.ApplicationVersion = appVersion;
 523
 40524            if (user is null)
 525            {
 0526                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 527            }
 528
 40529            return sessionInfo;
 530        }
 531
 532        private SessionInfo CreateSessionInfo(
 533            string key,
 534            string appName,
 535            string appVersion,
 536            string deviceId,
 537            string deviceName,
 538            string remoteEndPoint,
 539            User user)
 540        {
 40541            var sessionInfo = new SessionInfo(this, _logger)
 40542            {
 40543                Client = appName,
 40544                DeviceId = deviceId,
 40545                ApplicationVersion = appVersion,
 40546                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 40547                ServerId = _appHost.SystemId
 40548            };
 549
 40550            var username = user?.Username;
 551
 40552            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 40553            sessionInfo.UserName = username;
 40554            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 40555            sessionInfo.RemoteEndPoint = remoteEndPoint;
 556
 40557            if (string.IsNullOrEmpty(deviceName))
 558            {
 0559                deviceName = "Network Device";
 560            }
 561
 40562            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 40563            {
 40564                DeviceId = deviceId
 40565            };
 40566            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 567            {
 40568                sessionInfo.DeviceName = deviceName;
 569            }
 570            else
 571            {
 0572                sessionInfo.DeviceName = deviceOptions.CustomName;
 0573                sessionInfo.HasCustomDeviceName = true;
 574            }
 575
 40576            return sessionInfo;
 577        }
 578
 579        private List<User> GetUsers(SessionInfo session)
 580        {
 0581            var users = new List<User>();
 582
 0583            if (session.UserId.IsEmpty())
 584            {
 0585                return users;
 586            }
 587
 0588            var user = _userManager.GetUserById(session.UserId);
 589
 0590            if (user is null)
 591            {
 0592                throw new InvalidOperationException("User not found");
 593            }
 594
 0595            users.Add(user);
 596
 0597            users.AddRange(session.AdditionalUsers
 0598                .Select(i => _userManager.GetUserById(i.UserId))
 0599                .Where(i => i is not null));
 600
 0601            return users;
 602        }
 603
 604        private void StartCheckTimers()
 605        {
 0606            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 607
 0608            if (_config.Configuration.InactiveSessionThreshold > 0)
 609            {
 0610                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 611            }
 612            else
 613            {
 0614                StopInactiveCheckTimer();
 615            }
 0616        }
 617
 618        private void StopIdleCheckTimer()
 619        {
 0620            if (_idleTimer is not null)
 621            {
 0622                _idleTimer.Dispose();
 0623                _idleTimer = null;
 624            }
 0625        }
 626
 627        private void StopInactiveCheckTimer()
 628        {
 0629            if (_inactiveTimer is not null)
 630            {
 0631                _inactiveTimer.Dispose();
 0632                _inactiveTimer = null;
 633            }
 0634        }
 635
 636        private async void CheckForIdlePlayback(object state)
 637        {
 0638            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 0639                .ToList();
 640
 0641            if (playingSessions.Count > 0)
 642            {
 0643                var idle = playingSessions
 0644                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 0645                    .ToList();
 646
 0647                foreach (var session in idle)
 648                {
 0649                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 650
 651                    try
 652                    {
 0653                        await OnPlaybackStopped(new PlaybackStopInfo
 0654                        {
 0655                            Item = session.NowPlayingItem,
 0656                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 0657                            SessionId = session.Id,
 0658                            MediaSourceId = session.PlayState?.MediaSourceId,
 0659                            PositionTicks = session.PlayState?.PositionTicks
 0660                        }).ConfigureAwait(false);
 0661                    }
 0662                    catch (Exception ex)
 663                    {
 0664                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 0665                    }
 666                }
 667            }
 668            else
 669            {
 0670                StopIdleCheckTimer();
 671            }
 0672        }
 673
 674        private async void CheckForInactiveSteams(object state)
 675        {
 0676            var inactiveSessions = Sessions.Where(i =>
 0677                    i.NowPlayingItem is not null
 0678                    && i.PlayState.IsPaused
 0679                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 680
 0681            foreach (var session in inactiveSessions)
 682            {
 0683                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 684
 685                try
 686                {
 0687                    await SendPlaystateCommand(
 0688                        session.Id,
 0689                        session.Id,
 0690                        new PlaystateRequest()
 0691                        {
 0692                            Command = PlaystateCommand.Stop,
 0693                            ControllingUserId = session.UserId.ToString(),
 0694                            SeekPositionTicks = session.PlayState?.PositionTicks
 0695                        },
 0696                        CancellationToken.None).ConfigureAwait(true);
 0697                }
 0698                catch (Exception ex)
 699                {
 0700                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 0701                }
 0702            }
 703
 0704            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 705
 0706            if (!playingSessions)
 707            {
 0708                StopInactiveCheckTimer();
 709            }
 0710        }
 711
 712        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 713        {
 0714            if (session is null)
 715            {
 0716                return null;
 717            }
 718
 0719            var item = session.FullNowPlayingItem;
 0720            if (item is not null && item.Id.Equals(itemId))
 721            {
 0722                return item;
 723            }
 724
 0725            item = _libraryManager.GetItemById(itemId);
 726
 0727            session.FullNowPlayingItem = item;
 728
 0729            return item;
 730        }
 731
 732        /// <summary>
 733        /// Resolves the item whose user data (playback position, played status) should be updated
 734        /// for a playback report. When an alternate version is played the client reports the displayed
 735        /// item as <c>ItemId</c> and the played version as <c>MediaSourceId</c>.
 736        /// </summary>
 737        /// <param name="libraryItem">The now playing (displayed) item.</param>
 738        /// <param name="mediaSourceId">The reported media source id.</param>
 739        /// <returns>The item to track progress against.</returns>
 740        private BaseItem GetProgressItem(BaseItem libraryItem, string mediaSourceId)
 741        {
 0742            if (libraryItem is Video libraryVideo
 0743                && !string.IsNullOrEmpty(mediaSourceId)
 0744                && Guid.TryParse(mediaSourceId, out var mediaSourceItemId)
 0745                && !mediaSourceItemId.Equals(libraryVideo.Id))
 746            {
 0747                var versionItem = libraryVideo.GetAlternateVersion(mediaSourceItemId);
 0748                if (versionItem is not null)
 749                {
 0750                    return versionItem;
 751                }
 752            }
 753
 0754            return libraryItem;
 755        }
 756
 757        /// <summary>
 758        /// Used to report that playback has started for an item.
 759        /// </summary>
 760        /// <param name="info">The info.</param>
 761        /// <returns>Task.</returns>
 762        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 763        public async Task OnPlaybackStart(PlaybackStartInfo info)
 764        {
 0765            CheckDisposed();
 766
 0767            ArgumentNullException.ThrowIfNull(info);
 768
 0769            var session = GetSession(info.SessionId);
 770
 0771            var libraryItem = info.ItemId.IsEmpty()
 0772                ? null
 0773                : GetNowPlayingItem(session, info.ItemId);
 774
 0775            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 776
 0777            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 778            {
 0779                ClearTranscodingInfo(session.DeviceId);
 780            }
 781
 0782            session.StartAutomaticProgress(info);
 783
 0784            var users = GetUsers(session);
 785
 0786            if (libraryItem is not null)
 787            {
 0788                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0789                foreach (var user in users)
 790                {
 0791                    OnPlaybackStart(user, progressItem);
 792                }
 793            }
 794
 0795            if (!string.IsNullOrEmpty(info.LiveStreamId))
 796            {
 0797                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 798            }
 799
 0800            var eventArgs = new PlaybackStartEventArgs
 0801            {
 0802                Item = libraryItem,
 0803                Users = users,
 0804                MediaSourceId = info.MediaSourceId,
 0805                MediaInfo = info.Item,
 0806                DeviceName = session.DeviceName,
 0807                ClientName = session.Client,
 0808                DeviceId = session.DeviceId,
 0809                Session = session,
 0810                PlaybackPositionTicks = info.PositionTicks,
 0811                PlaySessionId = info.PlaySessionId
 0812            };
 813
 0814            if (info.Item is not null)
 815            {
 0816                _logger.LogInformation(
 0817                    "User {0} started playback of '{1}' ({2} {3})",
 0818                    session.UserName,
 0819                    info.Item.Name,
 0820                    session.Client,
 0821                    session.ApplicationVersion);
 822            }
 823
 0824            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 825
 826            // Nothing to save here
 827            // Fire events to inform plugins
 0828            EventHelper.QueueEventIfNotNull(
 0829                PlaybackStart,
 0830                this,
 0831                eventArgs,
 0832                _logger);
 833
 0834            StartCheckTimers();
 0835        }
 836
 837        /// <summary>
 838        /// Called when [playback start].
 839        /// </summary>
 840        /// <param name="user">The user object.</param>
 841        /// <param name="item">The item.</param>
 842        private void OnPlaybackStart(User user, BaseItem item)
 843        {
 0844            var data = _userDataManager.GetUserData(user, item);
 845
 0846            data.PlayCount++;
 0847            data.LastPlayedDate = DateTime.UtcNow;
 848
 0849            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 850            {
 0851                data.Played = true;
 852            }
 853
 0854            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0855        }
 856
 857        /// <inheritdoc />
 858        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 859        {
 0860            return OnPlaybackProgress(info, false);
 861        }
 862
 863        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 864        {
 0865            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 866
 0867            if (!string.IsNullOrEmpty(playSessionId))
 868            {
 0869                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 870                {
 0871                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 872                    {
 0873                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 874                    }
 875
 0876                    activeSessionMappings[sessionId] = playSessionId;
 0877                    activeSessionMappings[playSessionId] = sessionId;
 878                }
 879            }
 880            else
 881            {
 0882                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 883                {
 0884                    activeSessionMappings[sessionId] = string.Empty;
 885                }
 886            }
 0887        }
 888
 889        /// <summary>
 890        /// Used to report playback progress for an item.
 891        /// </summary>
 892        /// <param name="info">The playback progress info.</param>
 893        /// <param name="isAutomated">Whether this is an automated update.</param>
 894        /// <returns>Task.</returns>
 895        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 896        {
 0897            CheckDisposed();
 898
 0899            ArgumentNullException.ThrowIfNull(info);
 900
 0901            var session = GetSession(info.SessionId, false);
 0902            if (session is null)
 903            {
 0904                return;
 905            }
 906
 0907            var libraryItem = info.ItemId.IsEmpty()
 0908                ? null
 0909                : GetNowPlayingItem(session, info.ItemId);
 910
 0911            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 912
 0913            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 914            {
 0915                ClearTranscodingInfo(session.DeviceId);
 916            }
 917
 0918            var users = GetUsers(session);
 919
 920            // only update saved user data on actual check-ins, not automated ones
 0921            if (libraryItem is not null && !isAutomated)
 922            {
 0923                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0924                foreach (var user in users)
 925                {
 0926                    OnPlaybackProgress(user, progressItem, info);
 927                }
 928            }
 929
 0930            if (!string.IsNullOrEmpty(info.LiveStreamId))
 931            {
 0932                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 933            }
 934
 0935            var eventArgs = new PlaybackProgressEventArgs
 0936            {
 0937                Item = libraryItem,
 0938                Users = users,
 0939                PlaybackPositionTicks = session.PlayState.PositionTicks,
 0940                MediaSourceId = session.PlayState.MediaSourceId,
 0941                MediaInfo = info.Item,
 0942                DeviceName = session.DeviceName,
 0943                ClientName = session.Client,
 0944                DeviceId = session.DeviceId,
 0945                IsPaused = info.IsPaused,
 0946                PlaySessionId = info.PlaySessionId,
 0947                IsAutomated = isAutomated,
 0948                Session = session
 0949            };
 950
 0951            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 952
 0953            PlaybackProgress?.Invoke(this, eventArgs);
 954
 0955            if (!isAutomated)
 956            {
 0957                session.StartAutomaticProgress(info);
 958            }
 959
 0960            StartCheckTimers();
 0961        }
 962
 963        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 964        {
 0965            var data = _userDataManager.GetUserData(user, item);
 966
 0967            var positionTicks = info.PositionTicks;
 968
 0969            var changed = false;
 970
 0971            if (positionTicks.HasValue)
 972            {
 0973                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0974                changed = true;
 975            }
 976
 0977            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0978            if (tracksChanged)
 979            {
 0980                changed = true;
 981            }
 982
 0983            if (changed)
 984            {
 0985                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 986
 987                // A completed version marks every alternate version played and clears their resume points, so the
 988                // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 989                // only persist while nothing has been completed yet.)
 0990                if (data.Played == true && item is Video playedVideo)
 991                {
 0992                    playedVideo.PropagatePlayedState(user, true);
 993                }
 994            }
 995
 0996            if ((!user.RememberAudioSelections && info.AudioStreamIndex.HasValue)
 0997                || (!user.RememberSubtitleSelections && info.SubtitleStreamIndex.HasValue))
 998            {
 0999                _userDataManager.ResetPlaybackStreamSelections(user, item);
 1000            }
 01001        }
 1002
 1003        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 1004        {
 01005            var changed = false;
 1006
 01007            if (user.RememberAudioSelections)
 1008            {
 01009                if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
 1010                {
 01011                    data.AudioStreamIndex = info.AudioStreamIndex;
 01012                    changed = true;
 1013                }
 1014            }
 1015            else
 1016            {
 01017                if (data.AudioStreamIndex.HasValue)
 1018                {
 01019                    data.AudioStreamIndex = null;
 01020                    changed = true;
 1021                }
 1022            }
 1023
 01024            if (user.RememberSubtitleSelections)
 1025            {
 01026                if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 1027                {
 01028                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 01029                    changed = true;
 1030                }
 1031            }
 1032            else
 1033            {
 01034                if (data.SubtitleStreamIndex.HasValue)
 1035                {
 01036                    data.SubtitleStreamIndex = null;
 01037                    changed = true;
 1038                }
 1039            }
 1040
 01041            return changed;
 1042        }
 1043
 1044        /// <summary>
 1045        /// Used to report that playback has ended for an item.
 1046        /// </summary>
 1047        /// <param name="info">The info.</param>
 1048        /// <returns>Task.</returns>
 1049        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1050        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1051        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1052        {
 01053            CheckDisposed();
 1054
 01055            ArgumentNullException.ThrowIfNull(info);
 1056
 01057            var session = GetSession(info.SessionId);
 1058
 01059            session.StopAutomaticProgress();
 1060
 01061            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1062            {
 1063                // Ensure live stream is cleaned up before throwing, to prevent tuner
 1064                // resource leaks when stalled clients report a negative PositionTicks.
 01065                if (!string.IsNullOrEmpty(info.LiveStreamId))
 1066                {
 01067                    await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1068                }
 1069
 01070                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1071            }
 1072
 01073            var libraryItem = info.ItemId.IsEmpty()
 01074                ? null
 01075                : GetNowPlayingItem(session, info.ItemId);
 1076
 1077            // Normalize
 01078            if (string.IsNullOrEmpty(info.MediaSourceId))
 1079            {
 01080                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1081            }
 1082
 01083            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1084            {
 01085                var current = session.NowPlayingItem;
 1086
 01087                if (current is null || !info.ItemId.Equals(current.Id))
 1088                {
 01089                    MediaSourceInfo mediaSource = null;
 1090
 01091                    if (libraryItem is IHasMediaSources)
 1092                    {
 01093                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1094                    }
 1095
 01096                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1097                }
 1098                else
 1099                {
 01100                    info.Item = current;
 1101                }
 1102            }
 1103
 01104            if (info.Item is not null)
 1105            {
 01106                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1107
 01108                _logger.LogInformation(
 01109                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 01110                    session.UserName,
 01111                    info.Item.Name,
 01112                    msString,
 01113                    session.Client,
 01114                    session.ApplicationVersion);
 1115            }
 1116
 01117            if (info.NowPlayingQueue is not null)
 1118            {
 01119                session.NowPlayingQueue = info.NowPlayingQueue;
 1120            }
 1121
 01122            session.PlaylistItemId = info.PlaylistItemId;
 1123
 01124            RemoveNowPlayingItem(session);
 1125
 01126            var users = GetUsers(session);
 01127            var playedToCompletion = false;
 1128
 01129            if (libraryItem is not null)
 1130            {
 01131                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 01132                foreach (var user in users)
 1133                {
 01134                    playedToCompletion = OnPlaybackStopped(user, progressItem, info.PositionTicks, info.Failed);
 1135                }
 1136            }
 1137
 01138            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1139            {
 01140                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1141            }
 1142
 01143            var eventArgs = new PlaybackStopEventArgs
 01144            {
 01145                Item = libraryItem,
 01146                Users = users,
 01147                PlaybackPositionTicks = info.PositionTicks,
 01148                PlayedToCompletion = playedToCompletion,
 01149                MediaSourceId = info.MediaSourceId,
 01150                MediaInfo = info.Item,
 01151                DeviceName = session.DeviceName,
 01152                ClientName = session.Client,
 01153                DeviceId = session.DeviceId,
 01154                Session = session,
 01155                PlaySessionId = info.PlaySessionId
 01156            };
 1157
 01158            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1159
 01160            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 01161        }
 1162
 1163        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1164        {
 01165            if (playbackFailed)
 1166            {
 01167                return false;
 1168            }
 1169
 01170            var data = _userDataManager.GetUserData(user, item);
 1171            bool playedToCompletion;
 01172            if (positionTicks.HasValue)
 1173            {
 01174                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1175            }
 1176            else
 1177            {
 1178                // If the client isn't able to report this, then we'll just have to make an assumption
 01179                data.PlayCount++;
 01180                data.Played = item.SupportsPlayedStatus;
 01181                data.PlaybackPositionTicks = 0;
 01182                playedToCompletion = true;
 1183            }
 1184
 01185            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1186
 1187            // A completed version marks every alternate version played and clears their resume points, so the
 1188            // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 1189            // only persist while nothing has been completed yet.)
 01190            if (data.Played == true && item is Video playedVideo)
 1191            {
 01192                playedVideo.PropagatePlayedState(user, true);
 1193            }
 1194
 01195            return playedToCompletion;
 1196        }
 1197
 1198        /// <summary>
 1199        /// Gets the session.
 1200        /// </summary>
 1201        /// <param name="sessionId">The session identifier.</param>
 1202        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1203        /// <returns>SessionInfo.</returns>
 1204        /// <exception cref="ResourceNotFoundException">
 1205        /// No session with an Id equal to <c>sessionId</c> was found
 1206        /// and <c>throwOnMissing</c> is <c>true</c>.
 1207        /// </exception>
 1208        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1209        {
 31210            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 31211            if (session is null && throwOnMissing)
 1212            {
 11213                throw new ResourceNotFoundException(
 11214                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1215            }
 1216
 21217            return session;
 1218        }
 1219
 1220        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1221        {
 1222            // Accept either device id or session id
 01223            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1224
 01225            if (session is null)
 1226            {
 01227                throw new ResourceNotFoundException(
 01228                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1229            }
 1230
 01231            return session;
 1232        }
 1233
 1234        /// <inheritdoc />
 1235        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1236        {
 161237            return new SessionInfoDto
 161238            {
 161239                PlayState = sessionInfo.PlayState,
 161240                AdditionalUsers = sessionInfo.AdditionalUsers,
 161241                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 161242                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 161243                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 161244                Id = sessionInfo.Id,
 161245                UserId = sessionInfo.UserId,
 161246                UserName = sessionInfo.UserName,
 161247                Client = sessionInfo.Client,
 161248                LastActivityDate = sessionInfo.LastActivityDate,
 161249                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 161250                LastPausedDate = sessionInfo.LastPausedDate,
 161251                DeviceName = sessionInfo.DeviceName,
 161252                DeviceType = sessionInfo.DeviceType,
 161253                NowPlayingItem = sessionInfo.NowPlayingItem,
 161254                NowViewingItem = sessionInfo.NowViewingItem,
 161255                DeviceId = sessionInfo.DeviceId,
 161256                ApplicationVersion = sessionInfo.ApplicationVersion,
 161257                TranscodingInfo = sessionInfo.TranscodingInfo,
 161258                IsActive = sessionInfo.IsActive,
 161259                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 161260                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 161261                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 161262                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 161263                PlaylistItemId = sessionInfo.PlaylistItemId,
 161264                ServerId = sessionInfo.ServerId,
 161265                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 161266                SupportedCommands = sessionInfo.SupportedCommands
 161267            };
 1268        }
 1269
 1270        /// <inheritdoc />
 1271        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1272        {
 01273            CheckDisposed();
 1274
 01275            var generalCommand = new GeneralCommand
 01276            {
 01277                Name = GeneralCommandType.DisplayMessage
 01278            };
 1279
 01280            generalCommand.Arguments["Header"] = command.Header;
 01281            generalCommand.Arguments["Text"] = command.Text;
 1282
 01283            if (command.TimeoutMs.HasValue)
 1284            {
 01285                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1286            }
 1287
 01288            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1289        }
 1290
 1291        /// <inheritdoc />
 1292        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1293        {
 01294            CheckDisposed();
 1295
 01296            var session = GetSessionToRemoteControl(sessionId);
 1297
 01298            if (!string.IsNullOrEmpty(controllingSessionId))
 1299            {
 01300                var controllingSession = GetSession(controllingSessionId);
 01301                AssertCanControl(session, controllingSession);
 1302            }
 1303
 01304            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1305        }
 1306
 1307        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1308        {
 21309            var controllers = session.SessionControllers;
 21310            var messageId = Guid.NewGuid();
 1311
 81312            foreach (var controller in controllers)
 1313            {
 21314                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1315            }
 21316        }
 1317
 1318        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1319        {
 1320            IEnumerable<Task> GetTasks()
 1321            {
 1322                var messageId = Guid.NewGuid();
 1323                foreach (var session in sessions)
 1324                {
 1325                    var controllers = session.SessionControllers;
 1326                    foreach (var controller in controllers)
 1327                    {
 1328                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1329                    }
 1330                }
 1331            }
 1332
 241333            return Task.WhenAll(GetTasks());
 1334        }
 1335
 1336        /// <inheritdoc />
 1337        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1338        {
 01339            CheckDisposed();
 1340
 01341            var session = GetSessionToRemoteControl(sessionId);
 1342
 01343            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1344
 1345            List<BaseItem> items;
 1346
 01347            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1348            {
 01349                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01350                    .ToList();
 1351
 01352                command.PlayCommand = PlayCommand.PlayNow;
 1353            }
 1354            else
 1355            {
 01356                var list = new List<BaseItem>();
 01357                foreach (var itemId in command.ItemIds)
 1358                {
 01359                    var subItems = TranslateItemForPlayback(itemId, user);
 01360                    list.AddRange(subItems);
 1361                }
 1362
 01363                items = list;
 1364            }
 1365
 01366            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1367            {
 01368                items.Shuffle();
 01369                command.PlayCommand = PlayCommand.PlayNow;
 1370            }
 1371
 01372            command.ItemIds = items.Select(i => i.Id).ToArray();
 1373
 01374            if (user is not null)
 1375            {
 01376                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1377                {
 01378                    throw new ArgumentException(
 01379                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1380                }
 1381            }
 1382
 01383            if (user is not null
 01384                && command.ItemIds.Length == 1
 01385                && user.EnableNextEpisodeAutoPlay
 01386                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1387            {
 01388                var series = episode.Series;
 01389                if (series is not null)
 1390                {
 01391                    var episodes = series.GetEpisodes(
 01392                            user,
 01393                            new DtoOptions(false)
 01394                            {
 01395                                EnableImages = false
 01396                            },
 01397                            user.DisplayMissingEpisodes)
 01398                        .Where(i => !i.IsVirtualItem)
 01399                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01400                        .ToList();
 1401
 01402                    if (episodes.Count > 0)
 1403                    {
 01404                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1405                    }
 1406                }
 1407            }
 1408
 01409            if (!string.IsNullOrEmpty(controllingSessionId))
 1410            {
 01411                var controllingSession = GetSession(controllingSessionId);
 01412                AssertCanControl(session, controllingSession);
 01413                if (!controllingSession.UserId.IsEmpty())
 1414                {
 01415                    command.ControllingUserId = controllingSession.UserId;
 1416                }
 1417            }
 1418
 01419            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 01420        }
 1421
 1422        /// <inheritdoc />
 1423        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1424        {
 11425            CheckDisposed();
 11426            var session = GetSession(sessionId);
 11427            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 11428        }
 1429
 1430        /// <inheritdoc />
 1431        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1432        {
 21433            CheckDisposed();
 21434            var session = GetSession(sessionId);
 11435            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 11436        }
 1437
 1438        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1439        {
 01440            var item = _libraryManager.GetItemById(id);
 1441
 01442            if (item is null)
 1443            {
 01444                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01445                return Array.Empty<BaseItem>();
 1446            }
 1447
 01448            if (item is IItemByName byName)
 1449            {
 01450                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01451                {
 01452                    IsFolder = false,
 01453                    Recursive = true,
 01454                    DtoOptions = new DtoOptions(false)
 01455                    {
 01456                        EnableImages = false,
 01457                        Fields = new[]
 01458                        {
 01459                            ItemFields.SortName
 01460                        }
 01461                    },
 01462                    IsVirtualItem = false,
 01463                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01464                });
 1465            }
 1466
 01467            if (item.IsFolder)
 1468            {
 01469                var folder = (Folder)item;
 1470
 01471                return folder.GetItemList(new InternalItemsQuery(user)
 01472                {
 01473                    Recursive = true,
 01474                    IsFolder = false,
 01475                    DtoOptions = new DtoOptions(false)
 01476                    {
 01477                        EnableImages = false,
 01478                        Fields = new ItemFields[]
 01479                        {
 01480                            ItemFields.SortName
 01481                        }
 01482                    },
 01483                    IsVirtualItem = false,
 01484                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01485                });
 1486            }
 1487
 01488            return new[] { item };
 1489        }
 1490
 1491        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1492        {
 01493            var item = _libraryManager.GetItemById(id);
 1494
 01495            if (item is null)
 1496            {
 01497                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01498                return new List<BaseItem>();
 1499            }
 1500
 01501            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1502        }
 1503
 1504        /// <inheritdoc />
 1505        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1506        {
 01507            var generalCommand = new GeneralCommand
 01508            {
 01509                Name = GeneralCommandType.DisplayContent,
 01510                Arguments =
 01511                {
 01512                    ["ItemId"] = command.ItemId,
 01513                    ["ItemName"] = command.ItemName,
 01514                    ["ItemType"] = command.ItemType.ToString()
 01515                }
 01516            };
 1517
 01518            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1519        }
 1520
 1521        /// <inheritdoc />
 1522        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1523        {
 01524            CheckDisposed();
 1525
 01526            var session = GetSessionToRemoteControl(sessionId);
 1527
 01528            if (!string.IsNullOrEmpty(controllingSessionId))
 1529            {
 01530                var controllingSession = GetSession(controllingSessionId);
 01531                AssertCanControl(session, controllingSession);
 01532                if (!controllingSession.UserId.IsEmpty())
 1533                {
 01534                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1535                }
 1536            }
 1537
 01538            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1539        }
 1540
 1541        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1542        {
 01543            ArgumentNullException.ThrowIfNull(session);
 1544
 01545            ArgumentNullException.ThrowIfNull(controllingSession);
 01546        }
 1547
 1548        /// <summary>
 1549        /// Sends the restart required message.
 1550        /// </summary>
 1551        /// <param name="cancellationToken">The cancellation token.</param>
 1552        /// <returns>Task.</returns>
 1553        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1554        {
 01555            CheckDisposed();
 1556
 01557            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1558        }
 1559
 1560        /// <summary>
 1561        /// Adds the additional user.
 1562        /// </summary>
 1563        /// <param name="sessionId">The session identifier.</param>
 1564        /// <param name="userId">The user identifier.</param>
 1565        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1566        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1567        public void AddAdditionalUser(string sessionId, Guid userId)
 1568        {
 01569            CheckDisposed();
 1570
 01571            var session = GetSession(sessionId);
 1572
 01573            if (session.UserId.Equals(userId))
 1574            {
 01575                throw new ArgumentException("The requested user is already the primary user of the session.");
 1576            }
 1577
 01578            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1579            {
 01580                var user = _userManager.GetUserById(userId);
 01581                var newUser = new SessionUserInfo
 01582                {
 01583                    UserId = userId,
 01584                    UserName = user.Username
 01585                };
 1586
 01587                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1588            }
 01589        }
 1590
 1591        /// <summary>
 1592        /// Removes the additional user.
 1593        /// </summary>
 1594        /// <param name="sessionId">The session identifier.</param>
 1595        /// <param name="userId">The user identifier.</param>
 1596        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1597        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1598        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1599        {
 01600            CheckDisposed();
 1601
 01602            var session = GetSession(sessionId);
 1603
 01604            if (session.UserId.Equals(userId))
 1605            {
 01606                throw new ArgumentException("The requested user is already the primary user of the session.");
 1607            }
 1608
 01609            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1610
 01611            if (user is not null)
 1612            {
 01613                var list = session.AdditionalUsers.ToList();
 01614                list.Remove(user);
 1615
 01616                session.AdditionalUsers = list.ToArray();
 1617            }
 01618        }
 1619
 1620        /// <summary>
 1621        /// Authenticates the new session.
 1622        /// </summary>
 1623        /// <param name="request">The authenticationrequest.</param>
 1624        /// <returns>The authentication result.</returns>
 1625        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1626        {
 161627            return AuthenticateNewSessionInternal(request, true);
 1628        }
 1629
 1630        /// <summary>
 1631        /// Directly authenticates the session without enforcing password.
 1632        /// </summary>
 1633        /// <param name="request">The authentication request.</param>
 1634        /// <returns>The authentication result.</returns>
 1635        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1636        {
 01637            return AuthenticateNewSessionInternal(request, false);
 1638        }
 1639
 1640        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1641        {
 241642            CheckDisposed();
 1643
 241644            ArgumentException.ThrowIfNullOrEmpty(request.App);
 221645            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 201646            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 181647            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1648
 161649            User user = null;
 161650            if (!request.UserId.IsEmpty())
 1651            {
 01652                user = _userManager.GetUserById(request.UserId);
 1653            }
 1654
 161655            user ??= _userManager.GetUserByName(request.Username);
 1656
 161657            if (enforcePassword)
 1658            {
 161659                user = await _userManager.AuthenticateUser(
 161660                    request.Username,
 161661                    request.Password,
 161662                    request.RemoteEndPoint,
 161663                    true).ConfigureAwait(false);
 1664            }
 1665
 161666            if (user is null)
 1667            {
 01668                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01669                throw new AuthenticationException("Invalid username or password entered.");
 1670            }
 1671
 161672            if (!string.IsNullOrEmpty(request.DeviceId)
 161673                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1674            {
 01675                throw new SecurityException("User is not allowed access from this device.");
 1676            }
 1677
 161678            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 161679            int maxActiveSessions = user.MaxActiveSessions;
 161680            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 161681            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1682            {
 01683                throw new SecurityException("User is at their maximum number of sessions.");
 1684            }
 1685
 161686            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1687
 161688            var session = await LogSessionActivity(
 161689                request.App,
 161690                request.AppVersion,
 161691                request.DeviceId,
 161692                request.DeviceName,
 161693                request.RemoteEndPoint,
 161694                user).ConfigureAwait(false);
 1695
 161696            var returnResult = new AuthenticationResult
 161697            {
 161698                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 161699                SessionInfo = ToSessionInfoDto(session),
 161700                AccessToken = token,
 161701                ServerId = _appHost.SystemId
 161702            };
 1703
 161704            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 161705            return returnResult;
 161706        }
 1707
 1708        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1709        {
 1710            // This should be validated above, but if it isn't don't delete all tokens.
 181711            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1712
 161713            var existing = _deviceManager.GetDevices(
 161714                new DeviceQuery
 161715                {
 161716                    DeviceId = deviceId,
 161717                    UserId = user.Id
 161718                }).Items;
 1719
 321720            foreach (var auth in existing)
 1721            {
 1722                try
 1723                {
 1724                    // Logout any existing sessions for the user on this device
 01725                    await Logout(auth).ConfigureAwait(false);
 01726                }
 01727                catch (Exception ex)
 1728                {
 01729                    _logger.LogError(ex, "Error while logging out existing session.");
 01730                }
 1731            }
 1732
 161733            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 161734            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1735
 161736            return device.AccessToken;
 161737        }
 1738
 1739        /// <inheritdoc />
 1740        public async Task Logout(string accessToken)
 1741        {
 01742            CheckDisposed();
 1743
 01744            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1745
 01746            var existing = _deviceManager.GetDevices(
 01747                new DeviceQuery
 01748                {
 01749                    Limit = 1,
 01750                    AccessToken = accessToken
 01751                }).Items;
 1752
 01753            if (existing.Count > 0)
 1754            {
 01755                await Logout(existing[0]).ConfigureAwait(false);
 1756            }
 01757        }
 1758
 1759        /// <inheritdoc />
 1760        public async Task Logout(Device device)
 1761        {
 01762            CheckDisposed();
 1763
 01764            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1765
 01766            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1767
 01768            var sessions = Sessions
 01769                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01770                .ToList();
 1771
 01772            foreach (var session in sessions)
 1773            {
 1774                try
 1775                {
 01776                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01777                }
 01778                catch (Exception ex)
 1779                {
 01780                    _logger.LogError(ex, "Error reporting session ended");
 01781                }
 1782            }
 01783        }
 1784
 1785        /// <inheritdoc />
 1786        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1787        {
 21788            CheckDisposed();
 1789
 21790            var existing = _deviceManager.GetDevices(new DeviceQuery
 21791            {
 21792                UserId = userId
 21793            });
 1794
 41795            foreach (var info in existing.Items)
 1796            {
 01797                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1798                {
 01799                    await Logout(info).ConfigureAwait(false);
 1800                }
 1801            }
 21802        }
 1803
 1804        /// <summary>
 1805        /// Reports the capabilities.
 1806        /// </summary>
 1807        /// <param name="sessionId">The session identifier.</param>
 1808        /// <param name="capabilities">The capabilities.</param>
 1809        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1810        {
 01811            CheckDisposed();
 1812
 01813            var session = GetSession(sessionId);
 1814
 01815            ReportCapabilities(session, capabilities, true);
 01816        }
 1817
 1818        private void ReportCapabilities(
 1819            SessionInfo session,
 1820            ClientCapabilities capabilities,
 1821            bool saveCapabilities)
 1822        {
 171823            session.Capabilities = capabilities;
 1824
 171825            if (saveCapabilities)
 1826            {
 01827                CapabilitiesChanged?.Invoke(
 01828                    this,
 01829                    new SessionEventArgs
 01830                    {
 01831                        SessionInfo = session
 01832                    });
 1833
 01834                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1835            }
 171836        }
 1837
 1838        /// <summary>
 1839        /// Converts a BaseItem to a BaseItemInfo.
 1840        /// </summary>
 1841        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1842        {
 01843            ArgumentNullException.ThrowIfNull(item);
 1844
 01845            var dtoOptions = _itemInfoDtoOptions;
 1846
 01847            if (_itemInfoDtoOptions is null)
 1848            {
 01849                dtoOptions = new DtoOptions
 01850                {
 01851                    AddProgramRecordingInfo = false
 01852                };
 1853
 01854                var fields = dtoOptions.Fields.ToList();
 1855
 01856                fields.Remove(ItemFields.CanDelete);
 01857                fields.Remove(ItemFields.CanDownload);
 01858                fields.Remove(ItemFields.ChildCount);
 01859                fields.Remove(ItemFields.CustomRating);
 01860                fields.Remove(ItemFields.DateLastMediaAdded);
 01861                fields.Remove(ItemFields.DateLastRefreshed);
 01862                fields.Remove(ItemFields.DateLastSaved);
 01863                fields.Remove(ItemFields.DisplayPreferencesId);
 01864                fields.Remove(ItemFields.Etag);
 01865                fields.Remove(ItemFields.ItemCounts);
 01866                fields.Remove(ItemFields.MediaSourceCount);
 01867                fields.Remove(ItemFields.MediaStreams);
 01868                fields.Remove(ItemFields.MediaSources);
 01869                fields.Remove(ItemFields.People);
 01870                fields.Remove(ItemFields.PlayAccess);
 01871                fields.Remove(ItemFields.People);
 01872                fields.Remove(ItemFields.ProductionLocations);
 01873                fields.Remove(ItemFields.RecursiveItemCount);
 01874                fields.Remove(ItemFields.RemoteTrailers);
 01875                fields.Remove(ItemFields.SeasonUserData);
 01876                fields.Remove(ItemFields.Settings);
 01877                fields.Remove(ItemFields.SortName);
 01878                fields.Remove(ItemFields.Tags);
 1879
 01880                dtoOptions.Fields = fields.ToArray();
 1881
 01882                _itemInfoDtoOptions = dtoOptions;
 1883            }
 1884
 01885            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1886
 01887            if (mediaSource is not null)
 1888            {
 01889                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1890            }
 1891
 01892            return info;
 1893        }
 1894
 1895        private string GetImageCacheTag(User user)
 1896        {
 1897            try
 1898            {
 01899                return _imageProcessor.GetImageCacheTag(user);
 1900            }
 01901            catch (Exception e)
 1902            {
 01903                _logger.LogError(e, "Error getting image information for profile image");
 01904                return null;
 1905            }
 01906        }
 1907
 1908        /// <inheritdoc />
 1909        public void ReportNowViewingItem(string sessionId, string itemId)
 1910        {
 01911            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1912
 01913            var item = _libraryManager.GetItemById(new Guid(itemId));
 01914            var session = GetSession(sessionId);
 1915
 01916            session.NowViewingItem = GetItemInfo(item, null);
 01917        }
 1918
 1919        /// <inheritdoc />
 1920        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1921        {
 01922            var session = Sessions.FirstOrDefault(i =>
 01923                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1924
 01925            if (session is not null)
 1926            {
 01927                session.TranscodingInfo = info;
 1928            }
 01929        }
 1930
 1931        /// <inheritdoc />
 1932        public void ClearTranscodingInfo(string deviceId)
 1933        {
 01934            ReportTranscodingInfo(deviceId, null);
 01935        }
 1936
 1937        /// <inheritdoc />
 1938        public SessionInfo GetSession(string deviceId, string client, string version)
 1939        {
 01940            return Sessions.FirstOrDefault(i =>
 01941                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01942                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1943        }
 1944
 1945        /// <inheritdoc />
 1946        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1947        {
 01948            ArgumentNullException.ThrowIfNull(info);
 1949
 01950            var user = info.UserId.IsEmpty()
 01951                ? null
 01952                : _userManager.GetUserById(info.UserId);
 1953
 01954            appVersion = string.IsNullOrEmpty(appVersion)
 01955                ? info.AppVersion
 01956                : appVersion;
 1957
 01958            var deviceName = info.DeviceName;
 01959            var appName = info.AppName;
 1960
 01961            if (string.IsNullOrEmpty(deviceId))
 1962            {
 01963                deviceId = info.DeviceId;
 1964            }
 1965
 1966            // Prevent argument exception
 01967            if (string.IsNullOrEmpty(appVersion))
 1968            {
 01969                appVersion = "1";
 1970            }
 1971
 01972            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1973        }
 1974
 1975        /// <inheritdoc />
 1976        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1977        {
 01978            var items = _deviceManager.GetDevices(new DeviceQuery
 01979            {
 01980                AccessToken = token,
 01981                Limit = 1
 01982            }).Items;
 1983
 01984            if (items.Count == 0)
 1985            {
 01986                return null;
 1987            }
 1988
 01989            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 01990        }
 1991
 1992        /// <inheritdoc/>
 1993        public IReadOnlyList<SessionInfoDto> GetSessions(
 1994            Guid userId,
 1995            string deviceId,
 1996            int? activeWithinSeconds,
 1997            Guid? controllableUserToCheck,
 1998            bool isApiKey)
 1999        {
 02000            var result = Sessions;
 02001            if (!string.IsNullOrEmpty(deviceId))
 2002            {
 02003                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2004            }
 2005
 02006            var userCanControlOthers = false;
 02007            var userIsAdmin = false;
 02008            User user = null;
 2009
 02010            if (isApiKey)
 2011            {
 02012                userCanControlOthers = true;
 02013                userIsAdmin = true;
 2014            }
 02015            else if (!userId.IsEmpty())
 2016            {
 02017                user = _userManager.GetUserById(userId);
 02018                if (user is not null)
 2019                {
 02020                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 02021                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 2022                }
 2023                else
 2024                {
 02025                    return [];
 2026                }
 2027            }
 2028
 02029            if (!controllableUserToCheck.IsNullOrEmpty())
 2030            {
 02031                result = result.Where(i => i.SupportsRemoteControl);
 2032
 02033                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 02034                if (controlledUser is null)
 2035                {
 02036                    return [];
 2037                }
 2038
 02039                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 2040                {
 2041                    // Controlled user has device sharing disabled
 02042                    result = result.Where(i => !i.UserId.IsEmpty());
 2043                }
 2044
 02045                if (!userCanControlOthers)
 2046                {
 2047                    // User cannot control other user's sessions, validate user id.
 02048                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2049                }
 2050
 02051                result = result.Where(i =>
 02052                {
 02053                    if (isApiKey)
 02054                    {
 02055                        return true;
 02056                    }
 02057
 02058                    if (user is null)
 02059                    {
 02060                        return false;
 02061                    }
 02062
 02063                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02064                });
 2065            }
 02066            else if (!userIsAdmin)
 2067            {
 2068                // Request isn't from administrator, limit to "own" sessions.
 02069                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2070            }
 2071
 02072            if (!userIsAdmin)
 2073            {
 2074                // Don't report acceleration type for non-admin users.
 02075                result = result.Select(r =>
 02076                {
 02077                    if (r.TranscodingInfo is not null)
 02078                    {
 02079                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02080                    }
 02081
 02082                    return r;
 02083                });
 2084            }
 2085
 02086            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2087            {
 02088                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02089                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2090            }
 2091
 02092            return result.Select(ToSessionInfoDto).ToList();
 2093        }
 2094
 2095        /// <inheritdoc />
 2096        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2097        {
 02098            CheckDisposed();
 2099
 02100            var adminUserIds = _userManager.GetUsers()
 02101                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02102                .Select(i => i.Id)
 02103                .ToList();
 2104
 02105            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2106        }
 2107
 2108        /// <inheritdoc />
 2109        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2110        {
 02111            CheckDisposed();
 2112
 02113            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2114
 02115            if (sessions.Count == 0)
 2116            {
 02117                return Task.CompletedTask;
 2118            }
 2119
 02120            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2121        }
 2122
 2123        /// <inheritdoc />
 2124        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2125        {
 22126            CheckDisposed();
 2127
 22128            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 22129            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2130        }
 2131
 2132        /// <inheritdoc />
 2133        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2134        {
 02135            CheckDisposed();
 2136
 02137            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2138
 02139            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2140        }
 2141
 2142        /// <inheritdoc />
 2143        public async ValueTask DisposeAsync()
 2144        {
 322145            if (_disposed)
 2146            {
 02147                return;
 2148            }
 2149
 642150            foreach (var session in _activeConnections.Values)
 2151            {
 02152                await session.DisposeAsync().ConfigureAwait(false);
 2153            }
 2154
 322155            if (_idleTimer is not null)
 2156            {
 02157                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02158                _idleTimer = null;
 2159            }
 2160
 322161            if (_inactiveTimer is not null)
 2162            {
 02163                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02164                _inactiveTimer = null;
 2165            }
 2166
 322167            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2168
 322169            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 322170            _disposed = true;
 322171        }
 2172
 2173        private async void OnApplicationStopping()
 2174        {
 222175            _logger.LogInformation("Sending shutdown notifications");
 2176            try
 2177            {
 222178                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2179
 222180                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 222181            }
 02182            catch (Exception ex)
 2183            {
 02184                _logger.LogError(ex, "Error sending server shutdown notifications");
 02185            }
 2186
 2187            // Close open websockets to allow Kestrel to shut down cleanly
 762188            foreach (var session in _activeConnections.Values)
 2189            {
 162190                await session.DisposeAsync().ConfigureAwait(false);
 2191            }
 2192
 222193            _activeConnections.Clear();
 222194            _activeLiveStreamSessions.Clear();
 222195        }
 2196    }
 2197}

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)
GetProgressItem(MediaBrowser.Controller.Entities.BaseItem,System.String)
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()