< 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
44%
Covered lines: 457
Uncovered lines: 574
Coverable lines: 1031
Total lines: 2285
Line coverage: 44.3%
Branch coverage
32%
Covered branches: 146
Total branches: 456
Branch coverage: 32%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 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: 21977/20/2026 - 12:13:26 AM Line coverage: 25.4% (254/998) Branch coverage: 13.4% (56/416) Total lines: 21977/21/2026 - 12:16:33 AM Line coverage: 25.3% (253/998) Branch coverage: 13.2% (55/416) Total lines: 21977/26/2026 - 12:17:51 AM Line coverage: 25.3% (253/997) Branch coverage: 13.2% (55/416) Total lines: 21968/2/2026 - 12:17:28 AM Line coverage: 25.1% (251/997) Branch coverage: 12.9% (54/416) Total lines: 21968/3/2026 - 12:16:46 AM Line coverage: 25.3% (253/997) Branch coverage: 13.2% (55/416) Total lines: 219610/29/2026 - 12:17:11 AM Line coverage: 44.3% (457/1031) Branch coverage: 32% (146/456) Total lines: 2285 5/16/2026 - 12:15:55 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: 21977/20/2026 - 12:13:26 AM Line coverage: 25.4% (254/998) Branch coverage: 13.4% (56/416) Total lines: 21977/21/2026 - 12:16:33 AM Line coverage: 25.3% (253/998) Branch coverage: 13.2% (55/416) Total lines: 21977/26/2026 - 12:17:51 AM Line coverage: 25.3% (253/997) Branch coverage: 13.2% (55/416) Total lines: 21968/2/2026 - 12:17:28 AM Line coverage: 25.1% (251/997) Branch coverage: 12.9% (54/416) Total lines: 21968/3/2026 - 12:16:46 AM Line coverage: 25.3% (253/997) Branch coverage: 13.2% (55/416) Total lines: 219610/29/2026 - 12:17:11 AM Line coverage: 44.3% (457/1031) Branch coverage: 32% (146/456) Total lines: 2285

Coverage delta

Coverage delta 19 -19

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()80%101088.46%
OnSessionControllerConnected(...)100%11100%
CloseIfNeededAsync()66.66%6685.71%
CloseLiveStreamIfNeededAsync()0%110100%
ReportSessionEnded()0%620%
GetMediaSource(...)100%210%
UpdateNowPlayingItem()0%702260%
RemoveNowPlayingItem(...)50%22100%
GetSessionKey(...)100%11100%
GetSessionInfo(...)77.77%1818100%
CreateSessionInfo(...)71.42%141487.5%
GetUsers(...)25%10427.27%
StartCheckTimers()0%4260%
StopIdleCheckTimer()0%620%
StopInactiveCheckTimer()0%620%
CheckForIdlePlayback()62.5%8880.95%
CheckForInactiveSteams()0%4260%
GetNowPlayingItem(...)50%7675%
GetProgressItem(...)0%110100%
OnPlaybackStart()0%210140%
OnPlaybackStart(...)75%8887.5%
OnPlaybackProgress(...)100%210%
UpdateLiveStreamActiveSessionMappings(...)0%110100%
OnPlaybackProgress()0%420200%
OnPlaybackProgress(...)0%506220%
UpdatePlaybackSettings(...)0%272160%
OnPlaybackStopped()50%533474.57%
OnPlaybackStopped(...)58.33%201262.5%
GetSession(...)100%22100%
GetSessionToRemoteControl(...)50%2260%
ToSessionInfoDto(...)100%11100%
SendMessageCommand(...)50%2290%
SendGeneralCommand(...)50%22100%
SendMessageToSession()100%22100%
SendMessageToSessions(...)100%11100%
SendPlayCommand()53.57%1792842.22%
SendSyncPlayCommand()100%11100%
SendSyncPlayGroupUpdate()100%11100%
TranslateItemForPlayback(...)33.33%11647.36%
TranslateItemForInstantMix(...)0%620%
SendBrowseCommand(...)100%210%
SendPlaystateCommand(...)0%2040%
AssertCanControl(...)70%1010100%
AssertCanAttachUser(...)62.5%8887.5%
SendRestartRequiredNotification(...)100%210%
AddAdditionalUser(...)50%101094.44%
RemoveAdditionalUser(...)16.66%17633.33%
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(...)50%2266.66%
ReportCapabilities(...)25%9430%
GetItemInfo(...)0%2040%
GetImageCacheTag(...)100%210%
ReportNowViewingItem(...)0%620%
ReportTranscodingInfo(...)50%22100%
ClearTranscodingInfo(...)100%11100%
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()62.5%11864.28%
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;
 5064        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 5065            = new(StringComparer.OrdinalIgnoreCase);
 66
 5067        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
 5068            = 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        {
 50107            _logger = logger;
 50108            _eventManager = eventManager;
 50109            _userDataManager = userDataManager;
 50110            _config = serverConfigurationManager;
 50111            _libraryManager = libraryManager;
 50112            _userManager = userManager;
 50113            _musicManager = musicManager;
 50114            _dtoService = dtoService;
 50115            _imageProcessor = imageProcessor;
 50116            _appHost = appHost;
 50117            _deviceManager = deviceManager;
 50118            _mediaSourceManager = mediaSourceManager;
 50119            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 120
 50121            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 50122        }
 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>
 69158        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        {
 152181            ObjectDisposedException.ThrowIf(_disposed, this);
 152182        }
 183
 184        private void OnSessionStarted(SessionInfo info)
 185        {
 32186            if (!string.IsNullOrEmpty(info.DeviceId))
 187            {
 32188                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 189
 32190                if (capabilities is not null)
 191                {
 17192                    ReportCapabilities(info, capabilities, false);
 193                }
 194            }
 195
 32196            _eventManager.Publish(new SessionStartedEventArgs(info));
 197
 32198            EventHelper.QueueEventIfNotNull(
 32199                SessionStarted,
 32200                this,
 32201                new SessionEventArgs
 32202                {
 32203                    SessionInfo = info
 32204                },
 32205                _logger);
 32206        }
 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        {
 54252            CheckDisposed();
 253
 54254            ArgumentException.ThrowIfNullOrEmpty(appName);
 54255            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 54256            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 257
 54258            var activityDate = DateTime.UtcNow;
 54259            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 54260            var lastActivityDate = session.LastActivityDate;
 54261            session.LastActivityDate = activityDate;
 262
 54263            if (user is not null)
 264            {
 51265                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 266
 51267                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 268                {
 269                    try
 270                    {
 12271                        user.LastActivityDate = activityDate;
 12272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 12273                    }
 0274                    catch (DbUpdateConcurrencyException)
 275                    {
 0276                        _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is 
 0277                    }
 278                }
 279            }
 280
 54281            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 282            {
 32283                SessionActivity?.Invoke(
 32284                    this,
 32285                    new SessionEventArgs
 32286                    {
 32287                        SessionInfo = session
 32288                    });
 289            }
 290
 54291            return session;
 54292        }
 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, session.UserId);
 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, session.UserId);
 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        {
 2468            session.NowPlayingItem = null;
 2469            session.FullNowPlayingItem = null;
 2470            session.PlayState = new PlayerStateInfo();
 471
 2472            if (!string.IsNullOrEmpty(session.DeviceId))
 473            {
 2474                ClearTranscodingInfo(session.DeviceId);
 475            }
 2476        }
 477
 478        // The user is part of the key because the client name and the device id are taken from the
 479        // request headers and are not bound to the access token. Without it, any authenticated user
 480        // could claim another user's client/device pair and take over their session.
 481        private static string GetSessionKey(string appName, string deviceId, Guid userId)
 55482            => appName + deviceId + userId.ToString("N", CultureInfo.InvariantCulture);
 483
 484        /// <summary>
 485        /// Gets the connection.
 486        /// </summary>
 487        /// <param name="appName">Type of the client.</param>
 488        /// <param name="appVersion">The app version.</param>
 489        /// <param name="deviceId">The device id.</param>
 490        /// <param name="deviceName">Name of the device.</param>
 491        /// <param name="remoteEndPoint">The remote end point.</param>
 492        /// <param name="user">The user.</param>
 493        /// <returns>SessionInfo.</returns>
 494        private SessionInfo GetSessionInfo(
 495            string appName,
 496            string appVersion,
 497            string deviceId,
 498            string deviceName,
 499            string remoteEndPoint,
 500            User user)
 501        {
 54502            CheckDisposed();
 503
 54504            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 505
 54506            var key = GetSessionKey(appName, deviceId, user?.Id ?? Guid.Empty);
 54507            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 54508            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 54509            if (ReferenceEquals(newSession, sessionInfo))
 510            {
 32511                OnSessionStarted(newSession);
 512            }
 513
 54514            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 54515            sessionInfo.UserName = user?.Username;
 54516            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 54517            sessionInfo.RemoteEndPoint = remoteEndPoint;
 54518            sessionInfo.Client = appName;
 519
 54520            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 521            {
 54522                sessionInfo.DeviceName = deviceName;
 523            }
 524
 54525            sessionInfo.ApplicationVersion = appVersion;
 526
 54527            if (user is null)
 528            {
 3529                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 530            }
 531
 54532            return sessionInfo;
 533        }
 534
 535        private SessionInfo CreateSessionInfo(
 536            string key,
 537            string appName,
 538            string appVersion,
 539            string deviceId,
 540            string deviceName,
 541            string remoteEndPoint,
 542            User user)
 543        {
 54544            var sessionInfo = new SessionInfo(this, _logger)
 54545            {
 54546                Client = appName,
 54547                DeviceId = deviceId,
 54548                ApplicationVersion = appVersion,
 54549                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 54550                ServerId = _appHost.SystemId
 54551            };
 552
 54553            var username = user?.Username;
 554
 54555            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 54556            sessionInfo.UserName = username;
 54557            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 54558            sessionInfo.RemoteEndPoint = remoteEndPoint;
 559
 54560            if (string.IsNullOrEmpty(deviceName))
 561            {
 0562                deviceName = "Network Device";
 563            }
 564
 54565            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 54566            {
 54567                DeviceId = deviceId
 54568            };
 54569            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 570            {
 54571                sessionInfo.DeviceName = deviceName;
 572            }
 573            else
 574            {
 0575                sessionInfo.DeviceName = deviceOptions.CustomName;
 0576                sessionInfo.HasCustomDeviceName = true;
 577            }
 578
 54579            return sessionInfo;
 580        }
 581
 582        private List<User> GetUsers(SessionInfo session)
 583        {
 2584            var users = new List<User>();
 585
 2586            if (session.UserId.IsEmpty())
 587            {
 2588                return users;
 589            }
 590
 0591            var user = _userManager.GetUserById(session.UserId);
 592
 0593            if (user is null)
 594            {
 0595                throw new InvalidOperationException("User not found");
 596            }
 597
 0598            users.Add(user);
 599
 0600            users.AddRange(session.AdditionalUsers
 0601                .Select(i => _userManager.GetUserById(i.UserId))
 0602                .Where(i => i is not null));
 603
 0604            return users;
 605        }
 606
 607        private void StartCheckTimers()
 608        {
 0609            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 610
 0611            if (_config.Configuration.InactiveSessionThreshold > 0)
 612            {
 0613                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 614            }
 615            else
 616            {
 0617                StopInactiveCheckTimer();
 618            }
 0619        }
 620
 621        private void StopIdleCheckTimer()
 622        {
 0623            if (_idleTimer is not null)
 624            {
 0625                _idleTimer.Dispose();
 0626                _idleTimer = null;
 627            }
 0628        }
 629
 630        private void StopInactiveCheckTimer()
 631        {
 0632            if (_inactiveTimer is not null)
 633            {
 0634                _inactiveTimer.Dispose();
 0635                _inactiveTimer = null;
 636            }
 0637        }
 638
 639        private async void CheckForIdlePlayback(object state)
 640        {
 2641            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 2642                .ToList();
 643
 2644            if (playingSessions.Count > 0)
 645            {
 2646                var idle = playingSessions
 2647                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5);
 648
 8649                foreach (var session in idle)
 650                {
 2651                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 652
 653                    try
 654                    {
 2655                        await OnPlaybackStopped(new PlaybackStopInfo
 2656                        {
 2657                            Item = session.NowPlayingItem,
 2658                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 2659                            SessionId = session.Id,
 2660                            MediaSourceId = session.PlayState?.MediaSourceId,
 2661                            PositionTicks = session.LastPlaybackCheckInPositionTicks
 2662                        }).ConfigureAwait(false);
 2663                    }
 0664                    catch (Exception ex)
 665                    {
 0666                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 0667                    }
 668                }
 669            }
 670            else
 671            {
 0672                StopIdleCheckTimer();
 673            }
 2674        }
 675
 676        private async void CheckForInactiveSteams(object state)
 677        {
 0678            var inactiveSessions = Sessions.Where(i =>
 0679                    i.NowPlayingItem is not null
 0680                    && i.PlayState.IsPaused
 0681                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 682
 0683            foreach (var session in inactiveSessions)
 684            {
 0685                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 686
 687                try
 688                {
 0689                    await SendPlaystateCommand(
 0690                        session.Id,
 0691                        session.Id,
 0692                        new PlaystateRequest()
 0693                        {
 0694                            Command = PlaystateCommand.Stop,
 0695                            ControllingUserId = session.UserId.ToString(),
 0696                            SeekPositionTicks = session.PlayState?.PositionTicks
 0697                        },
 0698                        CancellationToken.None).ConfigureAwait(true);
 0699                }
 0700                catch (Exception ex)
 701                {
 0702                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 0703                }
 0704            }
 705
 0706            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 707
 0708            if (!playingSessions)
 709            {
 0710                StopInactiveCheckTimer();
 711            }
 0712        }
 713
 714        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 715        {
 2716            if (session is null)
 717            {
 0718                return null;
 719            }
 720
 2721            var item = session.FullNowPlayingItem;
 2722            if (item is not null && item.Id.Equals(itemId))
 723            {
 0724                return item;
 725            }
 726
 2727            item = _libraryManager.GetItemById(itemId);
 728
 2729            session.FullNowPlayingItem = item;
 730
 2731            return item;
 732        }
 733
 734        /// <summary>
 735        /// Resolves the item whose user data (playback position, played status) should be updated
 736        /// for a playback report. When an alternate version is played the client reports the displayed
 737        /// item as <c>ItemId</c> and the played version as <c>MediaSourceId</c>.
 738        /// </summary>
 739        /// <param name="libraryItem">The now playing (displayed) item.</param>
 740        /// <param name="mediaSourceId">The reported media source id.</param>
 741        /// <returns>The item to track progress against.</returns>
 742        private BaseItem GetProgressItem(BaseItem libraryItem, string mediaSourceId)
 743        {
 0744            if (libraryItem is Video libraryVideo
 0745                && !string.IsNullOrEmpty(mediaSourceId)
 0746                && Guid.TryParse(mediaSourceId, out var mediaSourceItemId)
 0747                && !mediaSourceItemId.Equals(libraryVideo.Id))
 748            {
 0749                var versionItem = libraryVideo.GetAlternateVersion(mediaSourceItemId);
 0750                if (versionItem is not null)
 751                {
 0752                    return versionItem;
 753                }
 754            }
 755
 0756            return libraryItem;
 757        }
 758
 759        /// <summary>
 760        /// Used to report that playback has started for an item.
 761        /// </summary>
 762        /// <param name="info">The info.</param>
 763        /// <returns>Task.</returns>
 764        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 765        public async Task OnPlaybackStart(PlaybackStartInfo info)
 766        {
 0767            CheckDisposed();
 768
 0769            ArgumentNullException.ThrowIfNull(info);
 770
 0771            var session = GetSession(info.SessionId);
 772
 0773            var libraryItem = info.ItemId.IsEmpty()
 0774                ? null
 0775                : GetNowPlayingItem(session, info.ItemId);
 776
 0777            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 778
 0779            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 780            {
 0781                ClearTranscodingInfo(session.DeviceId);
 782            }
 783
 0784            session.StartAutomaticProgress(info);
 785
 0786            var users = GetUsers(session);
 787
 0788            if (libraryItem is not null)
 789            {
 0790                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0791                foreach (var user in users)
 792                {
 0793                    OnPlaybackStart(user, progressItem);
 794                }
 795            }
 796
 0797            if (!string.IsNullOrEmpty(info.LiveStreamId))
 798            {
 0799                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 800            }
 801
 0802            var eventArgs = new PlaybackStartEventArgs
 0803            {
 0804                Item = libraryItem,
 0805                Users = users,
 0806                MediaSourceId = info.MediaSourceId,
 0807                MediaInfo = info.Item,
 0808                DeviceName = session.DeviceName,
 0809                ClientName = session.Client,
 0810                DeviceId = session.DeviceId,
 0811                Session = session,
 0812                PlaybackPositionTicks = info.PositionTicks,
 0813                PlaySessionId = info.PlaySessionId
 0814            };
 815
 0816            if (info.Item is not null)
 817            {
 0818                _logger.LogInformation(
 0819                    "User {0} started playback of '{1}' ({2} {3})",
 0820                    session.UserName,
 0821                    info.Item.Name,
 0822                    session.Client,
 0823                    session.ApplicationVersion);
 824            }
 825
 0826            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 827
 828            // Nothing to save here
 829            // Fire events to inform plugins
 0830            EventHelper.QueueEventIfNotNull(
 0831                PlaybackStart,
 0832                this,
 0833                eventArgs,
 0834                _logger);
 835
 0836            StartCheckTimers();
 0837        }
 838
 839        /// <summary>
 840        /// Called when [playback start].
 841        /// </summary>
 842        /// <param name="user">The user object.</param>
 843        /// <param name="item">The item.</param>
 844        private void OnPlaybackStart(User user, BaseItem item)
 845        {
 3846            var data = _userDataManager.GetUserData(user, item);
 847
 3848            data.PlayCount++;
 849
 850            // Re-watching a played item only counts once a progress or stop report gets past the resume threshold,
 851            // otherwise rewatch Next Up moves on from an episode that was barely started
 3852            if (!data.Played || !item.SupportsPositionTicksResume)
 853            {
 1854                data.LastPlayedDate = DateTime.UtcNow;
 855            }
 856
 3857            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 858            {
 0859                data.Played = true;
 860            }
 861
 3862            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 3863        }
 864
 865        /// <inheritdoc />
 866        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 867        {
 0868            return OnPlaybackProgress(info, false);
 869        }
 870
 871        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 872        {
 0873            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 874
 0875            if (!string.IsNullOrEmpty(playSessionId))
 876            {
 0877                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 878                {
 0879                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 880                    {
 0881                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 882                    }
 883
 0884                    activeSessionMappings[sessionId] = playSessionId;
 0885                    activeSessionMappings[playSessionId] = sessionId;
 886                }
 887            }
 888            else
 889            {
 0890                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 891                {
 0892                    activeSessionMappings[sessionId] = string.Empty;
 893                }
 894            }
 0895        }
 896
 897        /// <summary>
 898        /// Used to report playback progress for an item.
 899        /// </summary>
 900        /// <param name="info">The playback progress info.</param>
 901        /// <param name="isAutomated">Whether this is an automated update.</param>
 902        /// <returns>Task.</returns>
 903        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 904        {
 0905            CheckDisposed();
 906
 0907            ArgumentNullException.ThrowIfNull(info);
 908
 0909            var session = GetSession(info.SessionId, false);
 0910            if (session is null)
 911            {
 0912                return;
 913            }
 914
 0915            var libraryItem = info.ItemId.IsEmpty()
 0916                ? null
 0917                : GetNowPlayingItem(session, info.ItemId);
 918
 0919            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 920
 0921            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 922            {
 0923                ClearTranscodingInfo(session.DeviceId);
 924            }
 925
 0926            var users = GetUsers(session);
 927
 928            // only update saved user data on actual check-ins, not automated ones
 0929            if (libraryItem is not null && !isAutomated)
 930            {
 0931                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0932                foreach (var user in users)
 933                {
 0934                    OnPlaybackProgress(user, progressItem, info);
 935                }
 936            }
 937
 0938            if (!string.IsNullOrEmpty(info.LiveStreamId))
 939            {
 0940                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 941            }
 942
 0943            var eventArgs = new PlaybackProgressEventArgs
 0944            {
 0945                Item = libraryItem,
 0946                Users = users,
 0947                PlaybackPositionTicks = session.PlayState.PositionTicks,
 0948                MediaSourceId = session.PlayState.MediaSourceId,
 0949                MediaInfo = info.Item,
 0950                DeviceName = session.DeviceName,
 0951                ClientName = session.Client,
 0952                DeviceId = session.DeviceId,
 0953                IsPaused = info.IsPaused,
 0954                PlaySessionId = info.PlaySessionId,
 0955                IsAutomated = isAutomated,
 0956                Session = session
 0957            };
 958
 0959            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 960
 0961            PlaybackProgress?.Invoke(this, eventArgs);
 962
 0963            if (!isAutomated)
 964            {
 0965                session.StartAutomaticProgress(info);
 966            }
 967
 0968            StartCheckTimers();
 0969        }
 970
 971        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 972        {
 0973            var data = _userDataManager.GetUserData(user, item);
 974
 0975            var positionTicks = info.PositionTicks;
 976
 0977            var changed = false;
 978
 0979            if (positionTicks.HasValue)
 980            {
 0981                var playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0982                if (playedToCompletion || data.PlaybackPositionTicks > 0)
 983                {
 0984                    data.LastPlayedDate = DateTime.UtcNow;
 985                }
 986
 0987                changed = true;
 988            }
 989
 0990            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0991            if (tracksChanged)
 992            {
 0993                changed = true;
 994            }
 995
 0996            if (changed)
 997            {
 0998                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 999
 1000                // A completed version marks every alternate version played and clears their resume points, so the
 1001                // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 1002                // only persist while nothing has been completed yet.)
 01003                if (data.Played == true && item is Video playedVideo)
 1004                {
 01005                    playedVideo.PropagatePlayedState(user, true);
 1006                }
 1007            }
 1008
 01009            if ((!user.RememberAudioSelections && info.AudioStreamIndex.HasValue)
 01010                || (!user.RememberSubtitleSelections && info.SubtitleStreamIndex.HasValue))
 1011            {
 01012                _userDataManager.ResetPlaybackStreamSelections(user, item);
 1013            }
 01014        }
 1015
 1016        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 1017        {
 01018            var changed = false;
 1019
 01020            if (user.RememberAudioSelections)
 1021            {
 01022                if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
 1023                {
 01024                    data.AudioStreamIndex = info.AudioStreamIndex;
 01025                    changed = true;
 1026                }
 1027            }
 1028            else
 1029            {
 01030                if (data.AudioStreamIndex.HasValue)
 1031                {
 01032                    data.AudioStreamIndex = null;
 01033                    changed = true;
 1034                }
 1035            }
 1036
 01037            if (user.RememberSubtitleSelections)
 1038            {
 01039                if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 1040                {
 01041                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 01042                    changed = true;
 1043                }
 1044            }
 1045            else
 1046            {
 01047                if (data.SubtitleStreamIndex.HasValue)
 1048                {
 01049                    data.SubtitleStreamIndex = null;
 01050                    changed = true;
 1051                }
 1052            }
 1053
 01054            return changed;
 1055        }
 1056
 1057        /// <summary>
 1058        /// Used to report that playback has ended for an item.
 1059        /// </summary>
 1060        /// <param name="info">The info.</param>
 1061        /// <returns>Task.</returns>
 1062        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1063        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1064        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1065        {
 21066            CheckDisposed();
 1067
 21068            ArgumentNullException.ThrowIfNull(info);
 1069
 21070            var session = GetSession(info.SessionId);
 1071
 21072            session.StopAutomaticProgress();
 1073
 21074            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1075            {
 1076                // Ensure live stream is cleaned up before throwing, to prevent tuner
 1077                // resource leaks when stalled clients report a negative PositionTicks.
 01078                if (!string.IsNullOrEmpty(info.LiveStreamId))
 1079                {
 01080                    await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1081                }
 1082
 01083                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1084            }
 1085
 21086            var libraryItem = info.ItemId.IsEmpty()
 21087                ? null
 21088                : GetNowPlayingItem(session, info.ItemId);
 1089
 1090            // Normalize
 21091            if (string.IsNullOrEmpty(info.MediaSourceId))
 1092            {
 21093                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1094            }
 1095
 21096            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1097            {
 01098                var current = session.NowPlayingItem;
 1099
 01100                if (current is null || !info.ItemId.Equals(current.Id))
 1101                {
 01102                    MediaSourceInfo mediaSource = null;
 1103
 01104                    if (libraryItem is IHasMediaSources)
 1105                    {
 01106                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1107                    }
 1108
 01109                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1110                }
 1111                else
 1112                {
 01113                    info.Item = current;
 1114                }
 1115            }
 1116
 21117            if (info.Item is not null)
 1118            {
 21119                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1120
 21121                _logger.LogInformation(
 21122                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 21123                    session.UserName,
 21124                    info.Item.Name,
 21125                    msString,
 21126                    session.Client,
 21127                    session.ApplicationVersion);
 1128            }
 1129
 21130            if (info.NowPlayingQueue is not null)
 1131            {
 01132                session.NowPlayingQueue = info.NowPlayingQueue;
 1133            }
 1134
 21135            session.PlaylistItemId = info.PlaylistItemId;
 1136
 21137            RemoveNowPlayingItem(session);
 1138
 21139            var users = GetUsers(session);
 21140            var playedToCompletion = false;
 1141
 21142            if (libraryItem is not null)
 1143            {
 01144                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 01145                foreach (var user in users)
 1146                {
 01147                    playedToCompletion = OnPlaybackStopped(user, progressItem, info.PositionTicks, info.Failed);
 1148                }
 1149            }
 1150
 21151            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1152            {
 01153                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1154            }
 1155
 21156            var eventArgs = new PlaybackStopEventArgs
 21157            {
 21158                Item = libraryItem,
 21159                Users = users,
 21160                PlaybackPositionTicks = info.PositionTicks,
 21161                PlayedToCompletion = playedToCompletion,
 21162                MediaSourceId = info.MediaSourceId,
 21163                MediaInfo = info.Item,
 21164                DeviceName = session.DeviceName,
 21165                ClientName = session.Client,
 21166                DeviceId = session.DeviceId,
 21167                Session = session,
 21168                PlaySessionId = info.PlaySessionId
 21169            };
 1170
 21171            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1172
 21173            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 21174        }
 1175
 1176        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1177        {
 21178            if (playbackFailed)
 1179            {
 01180                return false;
 1181            }
 1182
 21183            var data = _userDataManager.GetUserData(user, item);
 1184            bool playedToCompletion;
 21185            if (positionTicks.HasValue)
 1186            {
 21187                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 21188                if (playedToCompletion || data.PlaybackPositionTicks > 0)
 1189                {
 11190                    data.LastPlayedDate = DateTime.UtcNow;
 1191                }
 1192            }
 1193            else
 1194            {
 1195                // If the client isn't able to report this, then we'll just have to make an assumption
 01196                data.PlayCount++;
 01197                data.Played = item.SupportsPlayedStatus;
 01198                data.PlaybackPositionTicks = 0;
 01199                data.LastPlayedDate = DateTime.UtcNow;
 01200                playedToCompletion = true;
 1201            }
 1202
 21203            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1204
 1205            // A completed version marks every alternate version played and clears their resume points, so the
 1206            // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 1207            // only persist while nothing has been completed yet.)
 21208            if (data.Played == true && item is Video playedVideo)
 1209            {
 21210                playedVideo.PropagatePlayedState(user, true);
 1211            }
 1212
 21213            return playedToCompletion;
 1214        }
 1215
 1216        /// <summary>
 1217        /// Gets the session.
 1218        /// </summary>
 1219        /// <param name="sessionId">The session identifier.</param>
 1220        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1221        /// <returns>SessionInfo.</returns>
 1222        /// <exception cref="ResourceNotFoundException">
 1223        /// No session with an Id equal to <c>sessionId</c> was found
 1224        /// and <c>throwOnMissing</c> is <c>true</c>.
 1225        /// </exception>
 1226        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1227        {
 151228            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 151229            if (session is null && throwOnMissing)
 1230            {
 11231                throw new ResourceNotFoundException(
 11232                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1233            }
 1234
 141235            return session;
 1236        }
 1237
 1238        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1239        {
 1240            // Accept either device id or session id
 31241            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1242
 31243            if (session is null)
 1244            {
 01245                throw new ResourceNotFoundException(
 01246                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1247            }
 1248
 31249            return session;
 1250        }
 1251
 1252        /// <inheritdoc />
 1253        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1254        {
 161255            return new SessionInfoDto
 161256            {
 161257                PlayState = sessionInfo.PlayState,
 161258                AdditionalUsers = sessionInfo.AdditionalUsers,
 161259                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 161260                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 161261                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 161262                Id = sessionInfo.Id,
 161263                UserId = sessionInfo.UserId,
 161264                UserName = sessionInfo.UserName,
 161265                Client = sessionInfo.Client,
 161266                LastActivityDate = sessionInfo.LastActivityDate,
 161267                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 161268                LastPausedDate = sessionInfo.LastPausedDate,
 161269                DeviceName = sessionInfo.DeviceName,
 161270                DeviceType = sessionInfo.DeviceType,
 161271                NowPlayingItem = sessionInfo.NowPlayingItem,
 161272                NowViewingItem = sessionInfo.NowViewingItem,
 161273                DeviceId = sessionInfo.DeviceId,
 161274                ApplicationVersion = sessionInfo.ApplicationVersion,
 161275                TranscodingInfo = sessionInfo.TranscodingInfo,
 161276                IsActive = sessionInfo.IsActive,
 161277                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 161278                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 161279                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 161280                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 161281                PlaylistItemId = sessionInfo.PlaylistItemId,
 161282                ServerId = sessionInfo.ServerId,
 161283                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 161284                SupportedCommands = sessionInfo.SupportedCommands
 161285            };
 1286        }
 1287
 1288        /// <inheritdoc />
 1289        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1290        {
 21291            CheckDisposed();
 1292
 21293            var generalCommand = new GeneralCommand
 21294            {
 21295                Name = GeneralCommandType.DisplayMessage
 21296            };
 1297
 21298            generalCommand.Arguments["Header"] = command.Header;
 21299            generalCommand.Arguments["Text"] = command.Text;
 1300
 21301            if (command.TimeoutMs.HasValue)
 1302            {
 01303                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1304            }
 1305
 21306            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1307        }
 1308
 1309        /// <inheritdoc />
 1310        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1311        {
 21312            CheckDisposed();
 1313
 21314            var session = GetSessionToRemoteControl(sessionId);
 1315
 21316            if (!string.IsNullOrEmpty(controllingSessionId))
 1317            {
 21318                var controllingSession = GetSession(controllingSessionId);
 21319                AssertCanControl(session, controllingSession);
 1320            }
 1321
 11322            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1323        }
 1324
 1325        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1326        {
 41327            var controllers = session.SessionControllers;
 41328            var messageId = Guid.NewGuid();
 1329
 121330            foreach (var controller in controllers)
 1331            {
 21332                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1333            }
 41334        }
 1335
 1336        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1337        {
 1338            IEnumerable<Task> GetTasks()
 1339            {
 1340                var messageId = Guid.NewGuid();
 1341                foreach (var session in sessions)
 1342                {
 1343                    var controllers = session.SessionControllers;
 1344                    foreach (var controller in controllers)
 1345                    {
 1346                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1347                    }
 1348                }
 1349            }
 1350
 311351            return Task.WhenAll(GetTasks());
 1352        }
 1353
 1354        /// <inheritdoc />
 1355        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1356        {
 11357            CheckDisposed();
 1358
 11359            var session = GetSessionToRemoteControl(sessionId);
 1360
 11361            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1362
 1363            List<BaseItem> items;
 1364
 11365            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1366            {
 01367                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01368                    .ToList();
 1369
 01370                command.PlayCommand = PlayCommand.PlayNow;
 1371            }
 1372            else
 1373            {
 11374                var list = new List<BaseItem>();
 41375                foreach (var itemId in command.ItemIds)
 1376                {
 11377                    var subItems = TranslateItemForPlayback(itemId, user);
 11378                    list.AddRange(subItems);
 1379                }
 1380
 11381                items = list;
 1382            }
 1383
 11384            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1385            {
 01386                items.Shuffle();
 01387                command.PlayCommand = PlayCommand.PlayNow;
 1388            }
 1389
 11390            command.ItemIds = items.Select(i => i.Id).ToArray();
 1391
 11392            if (user is not null)
 1393            {
 01394                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1395                {
 01396                    throw new ArgumentException(
 01397                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1398                }
 1399            }
 1400
 11401            if (user is not null
 11402                && command.ItemIds.Length == 1
 11403                && user.EnableNextEpisodeAutoPlay
 11404                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1405            {
 01406                var series = episode.Series;
 01407                if (series is not null)
 1408                {
 01409                    var episodes = series.GetEpisodes(
 01410                            user,
 01411                            new DtoOptions(false)
 01412                            {
 01413                                EnableImages = false
 01414                            },
 01415                            user.DisplayMissingEpisodes)
 01416                        .Where(i => !i.IsVirtualItem)
 01417                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01418                        .ToList();
 1419
 01420                    if (episodes.Count > 0)
 1421                    {
 01422                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1423                    }
 1424                }
 1425            }
 1426
 11427            if (!string.IsNullOrEmpty(controllingSessionId))
 1428            {
 01429                var controllingSession = GetSession(controllingSessionId);
 01430                AssertCanControl(session, controllingSession);
 01431                if (!controllingSession.UserId.IsEmpty())
 1432                {
 01433                    command.ControllingUserId = controllingSession.UserId;
 1434                }
 1435            }
 1436
 11437            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 11438        }
 1439
 1440        /// <inheritdoc />
 1441        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1442        {
 11443            CheckDisposed();
 11444            var session = GetSession(sessionId);
 11445            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 11446        }
 1447
 1448        /// <inheritdoc />
 1449        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1450        {
 21451            CheckDisposed();
 21452            var session = GetSession(sessionId);
 11453            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 11454        }
 1455
 1456        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1457        {
 11458            var item = _libraryManager.GetItemById(id);
 1459
 11460            if (item is null)
 1461            {
 01462                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01463                return Array.Empty<BaseItem>();
 1464            }
 1465
 11466            if (item is IItemByName byName)
 1467            {
 1468                // A by-name item tags containers as well as leaves: a music genre tags its artists,
 1469                // and a by-name artist row is not a folder, so IsFolder does not exclude it here.
 11470                return byName.GetTaggedItems(new InternalItemsQuery(user)
 11471                {
 11472                    IsFolder = false,
 11473                    Recursive = true,
 11474                    DtoOptions = new DtoOptions(false)
 11475                    {
 11476                        EnableImages = false,
 11477                        Fields = new[]
 11478                        {
 11479                            ItemFields.SortName
 11480                        }
 11481                    },
 11482                    IsVirtualItem = false,
 11483                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 11484                }).Where(i => i is not IItemByName);
 1485            }
 1486
 01487            if (item.IsFolder)
 1488            {
 01489                var folder = (Folder)item;
 1490
 01491                return folder.GetItemList(new InternalItemsQuery(user)
 01492                {
 01493                    Recursive = true,
 01494                    IsFolder = false,
 01495                    DtoOptions = new DtoOptions(false)
 01496                    {
 01497                        EnableImages = false,
 01498                        Fields = new ItemFields[]
 01499                        {
 01500                            ItemFields.SortName
 01501                        }
 01502                    },
 01503                    IsVirtualItem = false,
 01504                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01505                });
 1506            }
 1507
 01508            return new[] { item };
 1509        }
 1510
 1511        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1512        {
 01513            var item = _libraryManager.GetItemById(id);
 1514
 01515            if (item is null)
 1516            {
 01517                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01518                return new List<BaseItem>();
 1519            }
 1520
 01521            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1522        }
 1523
 1524        /// <inheritdoc />
 1525        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1526        {
 01527            var generalCommand = new GeneralCommand
 01528            {
 01529                Name = GeneralCommandType.DisplayContent,
 01530                Arguments =
 01531                {
 01532                    ["ItemId"] = command.ItemId,
 01533                    ["ItemName"] = command.ItemName,
 01534                    ["ItemType"] = command.ItemType.ToString()
 01535                }
 01536            };
 1537
 01538            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1539        }
 1540
 1541        /// <inheritdoc />
 1542        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1543        {
 01544            CheckDisposed();
 1545
 01546            var session = GetSessionToRemoteControl(sessionId);
 1547
 01548            if (!string.IsNullOrEmpty(controllingSessionId))
 1549            {
 01550                var controllingSession = GetSession(controllingSessionId);
 01551                AssertCanControl(session, controllingSession);
 01552                if (!controllingSession.UserId.IsEmpty())
 1553                {
 01554                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1555                }
 1556            }
 1557
 01558            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1559        }
 1560
 1561        private void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1562        {
 61563            ArgumentNullException.ThrowIfNull(session);
 1564
 61565            ArgumentNullException.ThrowIfNull(controllingSession);
 1566
 61567            var controllingUserId = controllingSession.UserId;
 1568
 1569            // Controlling a session is always allowed when:
 1570            // - the caller has no associated user (an API key, which is a privileged context),
 1571            // - the target session is public (has no owning user), or
 1572            // - the caller's user is associated with the target session.
 1573            // Controlling a session owned by a different user requires the
 1574            // EnableRemoteControlOfOtherUsers permission.
 61575            if (controllingUserId.IsEmpty()
 61576                || session.UserId.IsEmpty()
 61577                || session.ContainsUser(controllingUserId))
 1578            {
 21579                return;
 1580            }
 1581
 41582            var controllingUser = _userManager.GetUserById(controllingUserId);
 41583            if (controllingUser is null
 41584                || !controllingUser.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
 1585            {
 31586                throw new SecurityException("The current user does not have permission to remote control other users.");
 1587            }
 11588        }
 1589
 1590        private void AssertCanAttachUser(SessionInfo controllingSession, Guid userId)
 1591        {
 21592            var controllingUserId = controllingSession.UserId;
 1593
 1594            // Playback reported by a session is also written to the user data of its additional users,
 1595            // so attaching anyone but the calling user requires administrative privileges.
 21596            if (controllingUserId.IsEmpty() || controllingUserId.Equals(userId))
 1597            {
 01598                return;
 1599            }
 1600
 21601            var controllingUser = _userManager.GetUserById(controllingUserId);
 21602            if (controllingUser is null
 21603                || !controllingUser.HasPermission(PermissionKind.IsAdministrator))
 1604            {
 11605                throw new SecurityException("The current user does not have permission to attach another user to a sessi
 1606            }
 11607        }
 1608
 1609        /// <summary>
 1610        /// Sends the restart required message.
 1611        /// </summary>
 1612        /// <param name="cancellationToken">The cancellation token.</param>
 1613        /// <returns>Task.</returns>
 1614        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1615        {
 01616            CheckDisposed();
 1617
 01618            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1619        }
 1620
 1621        /// <summary>
 1622        /// Adds the additional user.
 1623        /// </summary>
 1624        /// <param name="controllingSessionId">The controlling session identifier.</param>
 1625        /// <param name="sessionId">The session identifier.</param>
 1626        /// <param name="userId">The user identifier.</param>
 1627        /// <exception cref="SecurityException">The controlling user is not allowed to attach the user to the session.</
 1628        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1629        public void AddAdditionalUser(string controllingSessionId, string sessionId, Guid userId)
 1630        {
 21631            CheckDisposed();
 1632
 21633            var session = GetSession(sessionId);
 1634
 21635            if (!string.IsNullOrEmpty(controllingSessionId))
 1636            {
 21637                var controllingSession = GetSession(controllingSessionId);
 21638                AssertCanControl(session, controllingSession);
 21639                AssertCanAttachUser(controllingSession, userId);
 1640            }
 1641
 11642            if (session.UserId.Equals(userId))
 1643            {
 01644                throw new ArgumentException("The requested user is already the primary user of the session.");
 1645            }
 1646
 11647            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1648            {
 11649                var user = _userManager.GetUserById(userId)
 11650                    ?? throw new ArgumentException("The requested user does not exist.");
 11651                var newUser = new SessionUserInfo
 11652                {
 11653                    UserId = userId,
 11654                    UserName = user.Username
 11655                };
 1656
 11657                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1658            }
 11659        }
 1660
 1661        /// <summary>
 1662        /// Removes the additional user.
 1663        /// </summary>
 1664        /// <param name="controllingSessionId">The controlling session identifier.</param>
 1665        /// <param name="sessionId">The session identifier.</param>
 1666        /// <param name="userId">The user identifier.</param>
 1667        /// <exception cref="SecurityException">The controlling user is not allowed to control the session.</exception>
 1668        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1669        public void RemoveAdditionalUser(string controllingSessionId, string sessionId, Guid userId)
 1670        {
 11671            CheckDisposed();
 1672
 11673            var session = GetSession(sessionId);
 1674
 11675            if (!string.IsNullOrEmpty(controllingSessionId))
 1676            {
 11677                AssertCanControl(session, GetSession(controllingSessionId));
 1678            }
 1679
 01680            if (session.UserId.Equals(userId))
 1681            {
 01682                throw new ArgumentException("The requested user is already the primary user of the session.");
 1683            }
 1684
 01685            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1686
 01687            if (user is not null)
 1688            {
 01689                var list = session.AdditionalUsers.ToList();
 01690                list.Remove(user);
 1691
 01692                session.AdditionalUsers = list.ToArray();
 1693            }
 01694        }
 1695
 1696        /// <summary>
 1697        /// Authenticates the new session.
 1698        /// </summary>
 1699        /// <param name="request">The authenticationrequest.</param>
 1700        /// <returns>The authentication result.</returns>
 1701        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1702        {
 161703            return AuthenticateNewSessionInternal(request, true);
 1704        }
 1705
 1706        /// <summary>
 1707        /// Directly authenticates the session without enforcing password.
 1708        /// </summary>
 1709        /// <param name="request">The authentication request.</param>
 1710        /// <returns>The authentication result.</returns>
 1711        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1712        {
 01713            return AuthenticateNewSessionInternal(request, false);
 1714        }
 1715
 1716        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1717        {
 241718            CheckDisposed();
 1719
 241720            ArgumentException.ThrowIfNullOrEmpty(request.App);
 221721            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 201722            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 181723            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1724
 161725            User user = null;
 161726            if (!request.UserId.IsEmpty())
 1727            {
 01728                user = _userManager.GetUserById(request.UserId);
 1729            }
 1730
 161731            user ??= _userManager.GetUserByName(request.Username);
 1732
 161733            if (enforcePassword)
 1734            {
 161735                user = await _userManager.AuthenticateUser(
 161736                    request.Username,
 161737                    request.Password,
 161738                    request.RemoteEndPoint,
 161739                    true).ConfigureAwait(false);
 1740            }
 1741
 161742            if (user is null)
 1743            {
 01744                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01745                throw new AuthenticationException("Invalid username or password entered.");
 1746            }
 1747
 161748            if (!string.IsNullOrEmpty(request.DeviceId)
 161749                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1750            {
 01751                throw new SecurityException("User is not allowed access from this device.");
 1752            }
 1753
 161754            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 161755            int maxActiveSessions = user.MaxActiveSessions;
 161756            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 161757            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1758            {
 01759                throw new SecurityException("User is at their maximum number of sessions.");
 1760            }
 1761
 161762            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1763
 161764            var session = await LogSessionActivity(
 161765                request.App,
 161766                request.AppVersion,
 161767                request.DeviceId,
 161768                request.DeviceName,
 161769                request.RemoteEndPoint,
 161770                user).ConfigureAwait(false);
 1771
 161772            var returnResult = new AuthenticationResult
 161773            {
 161774                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 161775                SessionInfo = ToSessionInfoDto(session),
 161776                AccessToken = token,
 161777                ServerId = _appHost.SystemId
 161778            };
 1779
 161780            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 161781            return returnResult;
 161782        }
 1783
 1784        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1785        {
 1786            // This should be validated above, but if it isn't don't delete all tokens.
 181787            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1788
 161789            var existing = _deviceManager.GetDevices(
 161790                new DeviceQuery
 161791                {
 161792                    DeviceId = deviceId,
 161793                    UserId = user.Id
 161794                }).Items;
 1795
 321796            foreach (var auth in existing)
 1797            {
 1798                try
 1799                {
 1800                    // Logout any existing sessions for the user on this device
 01801                    await Logout(auth).ConfigureAwait(false);
 01802                }
 01803                catch (Exception ex)
 1804                {
 01805                    _logger.LogError(ex, "Error while logging out existing session.");
 01806                }
 1807            }
 1808
 161809            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 161810            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1811
 161812            return device.AccessToken;
 161813        }
 1814
 1815        /// <inheritdoc />
 1816        public async Task Logout(string accessToken)
 1817        {
 01818            CheckDisposed();
 1819
 01820            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1821
 01822            var existing = _deviceManager.GetDevices(
 01823                new DeviceQuery
 01824                {
 01825                    Limit = 1,
 01826                    AccessToken = accessToken
 01827                }).Items;
 1828
 01829            if (existing.Count > 0)
 1830            {
 01831                await Logout(existing[0]).ConfigureAwait(false);
 1832            }
 01833        }
 1834
 1835        /// <inheritdoc />
 1836        public async Task Logout(Device device)
 1837        {
 01838            CheckDisposed();
 1839
 01840            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1841
 01842            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1843
 01844            var sessions = Sessions
 01845                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01846                .ToList();
 1847
 01848            foreach (var session in sessions)
 1849            {
 1850                try
 1851                {
 01852                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01853                }
 01854                catch (Exception ex)
 1855                {
 01856                    _logger.LogError(ex, "Error reporting session ended");
 01857                }
 1858            }
 01859        }
 1860
 1861        /// <inheritdoc />
 1862        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1863        {
 21864            CheckDisposed();
 1865
 21866            var existing = _deviceManager.GetDevices(new DeviceQuery
 21867            {
 21868                UserId = userId
 21869            });
 1870
 41871            foreach (var info in existing.Items)
 1872            {
 01873                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1874                {
 01875                    await Logout(info).ConfigureAwait(false);
 1876                }
 1877            }
 21878        }
 1879
 1880        /// <summary>
 1881        /// Reports the capabilities.
 1882        /// </summary>
 1883        /// <param name="controllingSessionId">The controlling session identifier.</param>
 1884        /// <param name="sessionId">The session identifier.</param>
 1885        /// <param name="capabilities">The capabilities.</param>
 1886        /// <exception cref="SecurityException">The controlling user is not allowed to control the session.</exception>
 1887        public void ReportCapabilities(string controllingSessionId, string sessionId, ClientCapabilities capabilities)
 1888        {
 11889            CheckDisposed();
 1890
 11891            var session = GetSession(sessionId);
 1892
 11893            if (!string.IsNullOrEmpty(controllingSessionId))
 1894            {
 11895                AssertCanControl(session, GetSession(controllingSessionId));
 1896            }
 1897
 01898            ReportCapabilities(session, capabilities, true);
 01899        }
 1900
 1901        private void ReportCapabilities(
 1902            SessionInfo session,
 1903            ClientCapabilities capabilities,
 1904            bool saveCapabilities)
 1905        {
 171906            session.Capabilities = capabilities;
 1907
 171908            if (saveCapabilities)
 1909            {
 01910                CapabilitiesChanged?.Invoke(
 01911                    this,
 01912                    new SessionEventArgs
 01913                    {
 01914                        SessionInfo = session
 01915                    });
 1916
 01917                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1918            }
 171919        }
 1920
 1921        /// <summary>
 1922        /// Converts a BaseItem to a BaseItemInfo.
 1923        /// </summary>
 1924        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1925        {
 01926            ArgumentNullException.ThrowIfNull(item);
 1927
 01928            var dtoOptions = _itemInfoDtoOptions;
 1929
 01930            if (_itemInfoDtoOptions is null)
 1931            {
 01932                dtoOptions = new DtoOptions
 01933                {
 01934                    AddProgramRecordingInfo = false
 01935                };
 1936
 01937                var fields = dtoOptions.Fields.ToList();
 1938
 01939                fields.Remove(ItemFields.CanDelete);
 01940                fields.Remove(ItemFields.CanDownload);
 01941                fields.Remove(ItemFields.ChildCount);
 01942                fields.Remove(ItemFields.CustomRating);
 01943                fields.Remove(ItemFields.DateLastMediaAdded);
 01944                fields.Remove(ItemFields.DateLastRefreshed);
 01945                fields.Remove(ItemFields.DateLastSaved);
 01946                fields.Remove(ItemFields.DisplayPreferencesId);
 01947                fields.Remove(ItemFields.Etag);
 01948                fields.Remove(ItemFields.ItemCounts);
 01949                fields.Remove(ItemFields.MediaSourceCount);
 01950                fields.Remove(ItemFields.MediaStreams);
 01951                fields.Remove(ItemFields.MediaSources);
 01952                fields.Remove(ItemFields.People);
 01953                fields.Remove(ItemFields.PlayAccess);
 01954                fields.Remove(ItemFields.People);
 01955                fields.Remove(ItemFields.ProductionLocations);
 01956                fields.Remove(ItemFields.RecursiveItemCount);
 01957                fields.Remove(ItemFields.RemoteTrailers);
 01958                fields.Remove(ItemFields.SeasonUserData);
 01959                fields.Remove(ItemFields.Settings);
 01960                fields.Remove(ItemFields.SortName);
 01961                fields.Remove(ItemFields.Tags);
 1962
 01963                dtoOptions.Fields = fields.ToArray();
 1964
 01965                _itemInfoDtoOptions = dtoOptions;
 1966            }
 1967
 01968            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1969
 01970            if (mediaSource is not null)
 1971            {
 01972                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1973            }
 1974
 01975            return info;
 1976        }
 1977
 1978        private string GetImageCacheTag(User user)
 1979        {
 1980            try
 1981            {
 01982                return _imageProcessor.GetImageCacheTag(user);
 1983            }
 01984            catch (Exception e)
 1985            {
 01986                _logger.LogError(e, "Error getting image information for profile image");
 01987                return null;
 1988            }
 01989        }
 1990
 1991        /// <inheritdoc />
 1992        public void ReportNowViewingItem(string controllingSessionId, string sessionId, string itemId)
 1993        {
 01994            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1995
 01996            var item = _libraryManager.GetItemById(new Guid(itemId));
 01997            var session = GetSession(sessionId);
 1998
 01999            if (!string.IsNullOrEmpty(controllingSessionId))
 2000            {
 02001                AssertCanControl(session, GetSession(controllingSessionId));
 2002            }
 2003
 02004            session.NowViewingItem = GetItemInfo(item, null);
 02005        }
 2006
 2007        /// <inheritdoc />
 2008        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 2009        {
 22010            var session = Sessions.FirstOrDefault(i =>
 22011                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2012
 22013            if (session is not null)
 2014            {
 22015                session.TranscodingInfo = info;
 2016            }
 22017        }
 2018
 2019        /// <inheritdoc />
 2020        public void ClearTranscodingInfo(string deviceId)
 2021        {
 22022            ReportTranscodingInfo(deviceId, null);
 22023        }
 2024
 2025        /// <inheritdoc />
 2026        public SessionInfo GetSession(string deviceId, string client, string version)
 2027        {
 02028            return Sessions.FirstOrDefault(i =>
 02029                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 02030                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 2031        }
 2032
 2033        /// <inheritdoc />
 2034        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 2035        {
 02036            ArgumentNullException.ThrowIfNull(info);
 2037
 02038            var user = info.UserId.IsEmpty()
 02039                ? null
 02040                : _userManager.GetUserById(info.UserId);
 2041
 02042            appVersion = string.IsNullOrEmpty(appVersion)
 02043                ? info.AppVersion
 02044                : appVersion;
 2045
 02046            var deviceName = info.DeviceName;
 02047            var appName = info.AppName;
 2048
 02049            if (string.IsNullOrEmpty(deviceId))
 2050            {
 02051                deviceId = info.DeviceId;
 2052            }
 2053
 2054            // Prevent argument exception
 02055            if (string.IsNullOrEmpty(appVersion))
 2056            {
 02057                appVersion = "1";
 2058            }
 2059
 02060            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 2061        }
 2062
 2063        /// <inheritdoc />
 2064        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 2065        {
 02066            var items = _deviceManager.GetDevices(new DeviceQuery
 02067            {
 02068                AccessToken = token,
 02069                Limit = 1
 02070            }).Items;
 2071
 02072            if (items.Count == 0)
 2073            {
 02074                return null;
 2075            }
 2076
 02077            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 02078        }
 2079
 2080        /// <inheritdoc/>
 2081        public IReadOnlyList<SessionInfoDto> GetSessions(
 2082            Guid userId,
 2083            string deviceId,
 2084            int? activeWithinSeconds,
 2085            Guid? controllableUserToCheck,
 2086            bool isApiKey)
 2087        {
 02088            var result = Sessions;
 02089            if (!string.IsNullOrEmpty(deviceId))
 2090            {
 02091                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2092            }
 2093
 02094            var userCanControlOthers = false;
 02095            var userIsAdmin = false;
 02096            User user = null;
 2097
 02098            if (isApiKey)
 2099            {
 02100                userCanControlOthers = true;
 02101                userIsAdmin = true;
 2102            }
 02103            else if (!userId.IsEmpty())
 2104            {
 02105                user = _userManager.GetUserById(userId);
 02106                if (user is not null)
 2107                {
 02108                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 02109                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 2110                }
 2111                else
 2112                {
 02113                    return [];
 2114                }
 2115            }
 2116
 02117            if (!controllableUserToCheck.IsNullOrEmpty())
 2118            {
 02119                result = result.Where(i => i.SupportsRemoteControl);
 2120
 02121                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 02122                if (controlledUser is null)
 2123                {
 02124                    return [];
 2125                }
 2126
 02127                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 2128                {
 2129                    // Controlled user has device sharing disabled
 02130                    result = result.Where(i => !i.UserId.IsEmpty());
 2131                }
 2132
 02133                if (!userCanControlOthers)
 2134                {
 2135                    // User cannot control other user's sessions, validate user id.
 02136                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2137                }
 2138
 02139                result = result.Where(i =>
 02140                {
 02141                    if (isApiKey)
 02142                    {
 02143                        return true;
 02144                    }
 02145
 02146                    if (user is null)
 02147                    {
 02148                        return false;
 02149                    }
 02150
 02151                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02152                });
 2153            }
 02154            else if (!userIsAdmin)
 2155            {
 2156                // Request isn't from administrator, limit to "own" sessions.
 02157                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2158            }
 2159
 02160            if (!userIsAdmin)
 2161            {
 2162                // Don't report acceleration type for non-admin users.
 02163                result = result.Select(r =>
 02164                {
 02165                    if (r.TranscodingInfo is not null)
 02166                    {
 02167                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02168                    }
 02169
 02170                    return r;
 02171                });
 2172            }
 2173
 02174            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2175            {
 02176                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02177                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2178            }
 2179
 02180            return result.Select(ToSessionInfoDto).ToList();
 2181        }
 2182
 2183        /// <inheritdoc />
 2184        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2185        {
 02186            CheckDisposed();
 2187
 02188            var adminUserIds = _userManager.GetUsers()
 02189                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02190                .Select(i => i.Id)
 02191                .ToList();
 2192
 02193            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2194        }
 2195
 2196        /// <inheritdoc />
 2197        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2198        {
 02199            CheckDisposed();
 2200
 02201            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2202
 02203            if (sessions.Count == 0)
 2204            {
 02205                return Task.CompletedTask;
 2206            }
 2207
 02208            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2209        }
 2210
 2211        /// <inheritdoc />
 2212        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2213        {
 42214            CheckDisposed();
 2215
 42216            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 42217            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2218        }
 2219
 2220        /// <inheritdoc />
 2221        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2222        {
 02223            CheckDisposed();
 2224
 02225            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2226
 02227            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2228        }
 2229
 2230        /// <inheritdoc />
 2231        public async ValueTask DisposeAsync()
 2232        {
 502233            if (_disposed)
 2234            {
 02235                return;
 2236            }
 2237
 1302238            foreach (var session in _activeConnections.Values)
 2239            {
 152240                await session.DisposeAsync().ConfigureAwait(false);
 2241            }
 2242
 502243            if (_idleTimer is not null)
 2244            {
 02245                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02246                _idleTimer = null;
 2247            }
 2248
 502249            if (_inactiveTimer is not null)
 2250            {
 02251                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02252                _inactiveTimer = null;
 2253            }
 2254
 502255            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2256
 502257            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 502258            _disposed = true;
 502259        }
 2260
 2261        private async void OnApplicationStopping()
 2262        {
 272263            _logger.LogInformation("Sending shutdown notifications");
 2264            try
 2265            {
 272266                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2267
 272268                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 272269            }
 02270            catch (Exception ex)
 2271            {
 02272                _logger.LogError(ex, "Error sending server shutdown notifications");
 02273            }
 2274
 2275            // Close open websockets to allow Kestrel to shut down cleanly
 862276            foreach (var session in _activeConnections.Values)
 2277            {
 162278                await session.DisposeAsync().ConfigureAwait(false);
 2279            }
 2280
 272281            _activeConnections.Clear();
 272282            _activeLiveStreamSessions.Clear();
 272283        }
 2284    }
 2285}

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,System.Guid)
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)
AssertCanAttachUser(MediaBrowser.Controller.Session.SessionInfo,System.Guid)
SendRestartRequiredNotification(System.Threading.CancellationToken)
AddAdditionalUser(System.String,System.String,System.Guid)
RemoveAdditionalUser(System.String,System.String,System.Guid)
AuthenticateNewSession(MediaBrowser.Controller.Session.AuthenticationRequest)
AuthenticateDirect(MediaBrowser.Controller.Session.AuthenticationRequest)
AuthenticateNewSessionInternal()
GetAuthorizationToken()
Logout()
Logout()
RevokeUserTokens()
ReportCapabilities(System.String,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,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()