< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Session.SessionManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Session/SessionManager.cs
Line coverage
25%
Covered lines: 253
Uncovered lines: 744
Coverable lines: 997
Total lines: 2196
Line coverage: 25.3%
Branch coverage
13%
Covered branches: 55
Total branches: 416
Branch coverage: 13.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

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

Coverage delta

Coverage delta 5 -5

Metrics

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

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Linq;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Queries;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Database.Implementations.Entities.Security;
 16using Jellyfin.Database.Implementations.Enums;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Events;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Devices;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Dto;
 26using MediaBrowser.Controller.Entities;
 27using MediaBrowser.Controller.Events;
 28using MediaBrowser.Controller.Events.Authentication;
 29using MediaBrowser.Controller.Events.Session;
 30using MediaBrowser.Controller.Library;
 31using MediaBrowser.Controller.Net;
 32using MediaBrowser.Controller.Session;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.Querying;
 37using MediaBrowser.Model.Session;
 38using MediaBrowser.Model.SyncPlay;
 39using Microsoft.EntityFrameworkCore;
 40using Microsoft.Extensions.Hosting;
 41using Microsoft.Extensions.Logging;
 42using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 43
 44namespace Emby.Server.Implementations.Session
 45{
 46    /// <summary>
 47    /// Class SessionManager.
 48    /// </summary>
 49    public sealed class SessionManager : ISessionManager, IAsyncDisposable
 50    {
 51        private readonly IUserDataManager _userDataManager;
 52        private readonly IServerConfigurationManager _config;
 53        private readonly ILogger<SessionManager> _logger;
 54        private readonly IEventManager _eventManager;
 55        private readonly ILibraryManager _libraryManager;
 56        private readonly IUserManager _userManager;
 57        private readonly IMusicManager _musicManager;
 58        private readonly IDtoService _dtoService;
 59        private readonly IImageProcessor _imageProcessor;
 60        private readonly IMediaSourceManager _mediaSourceManager;
 61        private readonly IServerApplicationHost _appHost;
 62        private readonly IDeviceManager _deviceManager;
 63        private readonly CancellationTokenRegistration _shutdownCallback;
 3264        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 3265            = new(StringComparer.OrdinalIgnoreCase);
 66
 3267        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
 3268            = new(StringComparer.OrdinalIgnoreCase);
 69
 70        private Timer _idleTimer;
 71        private Timer _inactiveTimer;
 72
 73        private DtoOptions _itemInfoDtoOptions;
 74        private bool _disposed;
 75
 76        /// <summary>
 77        /// Initializes a new instance of the <see cref="SessionManager"/> class.
 78        /// </summary>
 79        /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param>
 80        /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param>
 81        /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param>
 82        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</p
 83        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
 84        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
 85        /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param>
 86        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
 87        /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
 88        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
 89        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
 90        /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
 91        /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
 92        public SessionManager(
 93            ILogger<SessionManager> logger,
 94            IEventManager eventManager,
 95            IUserDataManager userDataManager,
 96            IServerConfigurationManager serverConfigurationManager,
 97            ILibraryManager libraryManager,
 98            IUserManager userManager,
 99            IMusicManager musicManager,
 100            IDtoService dtoService,
 101            IImageProcessor imageProcessor,
 102            IServerApplicationHost appHost,
 103            IDeviceManager deviceManager,
 104            IMediaSourceManager mediaSourceManager,
 105            IHostApplicationLifetime hostApplicationLifetime)
 106        {
 32107            _logger = logger;
 32108            _eventManager = eventManager;
 32109            _userDataManager = userDataManager;
 32110            _config = serverConfigurationManager;
 32111            _libraryManager = libraryManager;
 32112            _userManager = userManager;
 32113            _musicManager = musicManager;
 32114            _dtoService = dtoService;
 32115            _imageProcessor = imageProcessor;
 32116            _appHost = appHost;
 32117            _deviceManager = deviceManager;
 32118            _mediaSourceManager = mediaSourceManager;
 32119            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 120
 32121            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 32122        }
 123
 124        /// <summary>
 125        /// Occurs when playback has started.
 126        /// </summary>
 127        public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
 128
 129        /// <summary>
 130        /// Occurs when playback has progressed.
 131        /// </summary>
 132        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
 133
 134        /// <summary>
 135        /// Occurs when playback has stopped.
 136        /// </summary>
 137        public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 138
 139        /// <inheritdoc />
 140        public event EventHandler<SessionEventArgs> SessionStarted;
 141
 142        /// <inheritdoc />
 143        public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 144
 145        /// <inheritdoc />
 146        public event EventHandler<SessionEventArgs> SessionEnded;
 147
 148        /// <inheritdoc />
 149        public event EventHandler<SessionEventArgs> SessionActivity;
 150
 151        /// <inheritdoc />
 152        public event EventHandler<SessionEventArgs> SessionControllerConnected;
 153
 154        /// <summary>
 155        /// Gets all connections.
 156        /// </summary>
 157        /// <value>All connections.</value>
 43158        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate)
 159
 160        private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e
 161        {
 0162            foreach (var session in Sessions)
 163            {
 0164                if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
 165                {
 0166                    if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
 167                    {
 0168                        session.HasCustomDeviceName = true;
 0169                        session.DeviceName = e.Argument.Item2.CustomName;
 170                    }
 171                    else
 172                    {
 0173                        session.HasCustomDeviceName = false;
 174                    }
 175                }
 176            }
 0177        }
 178
 179        private void CheckDisposed()
 180        {
 111181            ObjectDisposedException.ThrowIf(_disposed, this);
 111182        }
 183
 184        private void OnSessionStarted(SessionInfo info)
 185        {
 17186            if (!string.IsNullOrEmpty(info.DeviceId))
 187            {
 17188                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 189
 17190                if (capabilities is not null)
 191                {
 17192                    ReportCapabilities(info, capabilities, false);
 193                }
 194            }
 195
 17196            _eventManager.Publish(new SessionStartedEventArgs(info));
 197
 17198            EventHelper.QueueEventIfNotNull(
 17199                SessionStarted,
 17200                this,
 17201                new SessionEventArgs
 17202                {
 17203                    SessionInfo = info
 17204                },
 17205                _logger);
 17206        }
 207
 208        private async ValueTask OnSessionEnded(SessionInfo info)
 209        {
 1210            EventHelper.QueueEventIfNotNull(
 1211                SessionEnded,
 1212                this,
 1213                new SessionEventArgs
 1214                {
 1215                    SessionInfo = info
 1216                },
 1217                _logger);
 218
 1219            _eventManager.Publish(new SessionEndedEventArgs(info));
 220
 1221            await info.DisposeAsync().ConfigureAwait(false);
 1222        }
 223
 224        /// <inheritdoc />
 225        public void UpdateDeviceName(string sessionId, string reportedDeviceName)
 226        {
 0227            var session = GetSession(sessionId);
 0228            if (session is not null)
 229            {
 0230                session.DeviceName = reportedDeviceName;
 231            }
 0232        }
 233
 234        /// <summary>
 235        /// Logs the user activity.
 236        /// </summary>
 237        /// <param name="appName">Type of the client.</param>
 238        /// <param name="appVersion">The app version.</param>
 239        /// <param name="deviceId">The device id.</param>
 240        /// <param name="deviceName">Name of the device.</param>
 241        /// <param name="remoteEndPoint">The remote end point.</param>
 242        /// <param name="user">The user.</param>
 243        /// <returns>SessionInfo.</returns>
 244        public async Task<SessionInfo> LogSessionActivity(
 245            string appName,
 246            string appVersion,
 247            string deviceId,
 248            string deviceName,
 249            string remoteEndPoint,
 250            User user)
 251        {
 40252            CheckDisposed();
 253
 40254            ArgumentException.ThrowIfNullOrEmpty(appName);
 40255            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 40256            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 257
 40258            var activityDate = DateTime.UtcNow;
 40259            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 40260            var lastActivityDate = session.LastActivityDate;
 40261            session.LastActivityDate = activityDate;
 262
 40263            if (user is not null)
 264            {
 40265                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 266
 40267                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 268                {
 269                    try
 270                    {
 0271                        user.LastActivityDate = activityDate;
 0272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 0273                    }
 0274                    catch (DbUpdateConcurrencyException)
 275                    {
 0276                        _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is 
 0277                    }
 278                }
 279            }
 280
 40281            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 282            {
 17283                SessionActivity?.Invoke(
 17284                    this,
 17285                    new SessionEventArgs
 17286                    {
 17287                        SessionInfo = session
 17288                    });
 289            }
 290
 40291            return session;
 40292        }
 293
 294        /// <inheritdoc />
 295        public void OnSessionControllerConnected(SessionInfo session)
 296        {
 1297            EventHelper.QueueEventIfNotNull(
 1298                SessionControllerConnected,
 1299                this,
 1300                new SessionEventArgs
 1301                {
 1302                    SessionInfo = session
 1303                },
 1304                _logger);
 1305        }
 306
 307        /// <inheritdoc />
 308        public async Task CloseIfNeededAsync(SessionInfo session)
 309        {
 1310            if (!session.SessionControllers.Any(i => i.IsSessionActive))
 311            {
 1312                var key = GetSessionKey(session.Client, session.DeviceId);
 313
 1314                _activeConnections.TryRemove(key, out _);
 1315                if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
 316                {
 0317                    await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false)
 318                }
 319
 1320                await OnSessionEnded(session).ConfigureAwait(false);
 321            }
 1322        }
 323
 324        /// <inheritdoc />
 325        public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
 326        {
 0327            bool liveStreamNeedsToBeClosed = false;
 328
 0329            if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
 330            {
 0331                if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
 332                {
 0333                    if (!string.IsNullOrEmpty(correspondingId))
 334                    {
 0335                        activeSessionMappings.TryRemove(correspondingId, out _);
 336                    }
 337
 0338                    liveStreamNeedsToBeClosed = true;
 339                }
 340
 0341                if (activeSessionMappings.IsEmpty)
 342                {
 0343                    _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
 344                }
 345            }
 346            else
 347            {
 0348                liveStreamNeedsToBeClosed = true;
 349            }
 350
 0351            if (liveStreamNeedsToBeClosed)
 352            {
 353                try
 354                {
 0355                    await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
 0356                }
 0357                catch (Exception ex)
 358                {
 0359                    _logger.LogError(ex, "Error closing live stream");
 0360                }
 361            }
 0362        }
 363
 364        /// <inheritdoc />
 365        public async ValueTask ReportSessionEnded(string sessionId)
 366        {
 0367            CheckDisposed();
 0368            var session = GetSession(sessionId, false);
 369
 0370            if (session is not null)
 371            {
 0372                var key = GetSessionKey(session.Client, session.DeviceId);
 373
 0374                _activeConnections.TryRemove(key, out _);
 375
 0376                await OnSessionEnded(session).ConfigureAwait(false);
 377            }
 0378        }
 379
 380        private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
 381        {
 0382            return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
 383        }
 384
 385        /// <summary>
 386        /// Updates the now playing item id.
 387        /// </summary>
 388        /// <returns>Task.</returns>
 389        private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bo
 390        {
 0391            if (session is null)
 392            {
 0393                return;
 394            }
 395
 0396            if (string.IsNullOrEmpty(info.MediaSourceId))
 397            {
 0398                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 399            }
 400
 0401            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 402            {
 0403                var current = session.NowPlayingItem;
 404
 0405                if (current is null || !info.ItemId.Equals(current.Id))
 406                {
 0407                    var runtimeTicks = libraryItem.RunTimeTicks;
 408
 0409                    MediaSourceInfo mediaSource = null;
 0410                    if (libraryItem is IHasMediaSources)
 411                    {
 0412                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 413
 0414                        if (mediaSource is not null)
 415                        {
 0416                            runtimeTicks = mediaSource.RunTimeTicks;
 417                        }
 418                    }
 419
 0420                    info.Item = GetItemInfo(libraryItem, mediaSource);
 421
 0422                    info.Item.RunTimeTicks = runtimeTicks;
 423                }
 424                else
 425                {
 0426                    info.Item = current;
 427                }
 428            }
 429
 0430            session.NowPlayingItem = info.Item;
 0431            session.LastActivityDate = DateTime.UtcNow;
 432
 0433            if (updateLastCheckInTime)
 434            {
 0435                session.LastPlaybackCheckIn = DateTime.UtcNow;
 436            }
 437
 0438            if (info.IsPaused && session.LastPausedDate is null)
 439            {
 0440                session.LastPausedDate = DateTime.UtcNow;
 441            }
 0442            else if (!info.IsPaused)
 443            {
 0444                session.LastPausedDate = null;
 445            }
 446
 0447            session.PlayState.IsPaused = info.IsPaused;
 0448            session.PlayState.PositionTicks = info.PositionTicks;
 0449            session.PlayState.MediaSourceId = info.MediaSourceId;
 0450            session.PlayState.LiveStreamId = info.LiveStreamId;
 0451            session.PlayState.CanSeek = info.CanSeek;
 0452            session.PlayState.IsMuted = info.IsMuted;
 0453            session.PlayState.VolumeLevel = info.VolumeLevel;
 0454            session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
 0455            session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0456            session.PlayState.PlayMethod = info.PlayMethod;
 0457            session.PlayState.RepeatMode = info.RepeatMode;
 0458            session.PlayState.PlaybackOrder = info.PlaybackOrder;
 0459            session.PlaylistItemId = info.PlaylistItemId;
 0460        }
 461
 462        /// <summary>
 463        /// Removes the now playing item id.
 464        /// </summary>
 465        /// <param name="session">The session.</param>
 466        private void RemoveNowPlayingItem(SessionInfo session)
 467        {
 0468            session.NowPlayingItem = null;
 0469            session.FullNowPlayingItem = null;
 0470            session.PlayState = new PlayerStateInfo();
 471
 0472            if (!string.IsNullOrEmpty(session.DeviceId))
 473            {
 0474                ClearTranscodingInfo(session.DeviceId);
 475            }
 0476        }
 477
 478        private static string GetSessionKey(string appName, string deviceId)
 41479            => appName + deviceId;
 480
 481        /// <summary>
 482        /// Gets the connection.
 483        /// </summary>
 484        /// <param name="appName">Type of the client.</param>
 485        /// <param name="appVersion">The app version.</param>
 486        /// <param name="deviceId">The device id.</param>
 487        /// <param name="deviceName">Name of the device.</param>
 488        /// <param name="remoteEndPoint">The remote end point.</param>
 489        /// <param name="user">The user.</param>
 490        /// <returns>SessionInfo.</returns>
 491        private SessionInfo GetSessionInfo(
 492            string appName,
 493            string appVersion,
 494            string deviceId,
 495            string deviceName,
 496            string remoteEndPoint,
 497            User user)
 498        {
 40499            CheckDisposed();
 500
 40501            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 502
 40503            var key = GetSessionKey(appName, deviceId);
 40504            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 40505            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 40506            if (ReferenceEquals(newSession, sessionInfo))
 507            {
 17508                OnSessionStarted(newSession);
 509            }
 510
 40511            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 40512            sessionInfo.UserName = user?.Username;
 40513            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 40514            sessionInfo.RemoteEndPoint = remoteEndPoint;
 40515            sessionInfo.Client = appName;
 516
 40517            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 518            {
 40519                sessionInfo.DeviceName = deviceName;
 520            }
 521
 40522            sessionInfo.ApplicationVersion = appVersion;
 523
 40524            if (user is null)
 525            {
 0526                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 527            }
 528
 40529            return sessionInfo;
 530        }
 531
 532        private SessionInfo CreateSessionInfo(
 533            string key,
 534            string appName,
 535            string appVersion,
 536            string deviceId,
 537            string deviceName,
 538            string remoteEndPoint,
 539            User user)
 540        {
 40541            var sessionInfo = new SessionInfo(this, _logger)
 40542            {
 40543                Client = appName,
 40544                DeviceId = deviceId,
 40545                ApplicationVersion = appVersion,
 40546                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 40547                ServerId = _appHost.SystemId
 40548            };
 549
 40550            var username = user?.Username;
 551
 40552            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 40553            sessionInfo.UserName = username;
 40554            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 40555            sessionInfo.RemoteEndPoint = remoteEndPoint;
 556
 40557            if (string.IsNullOrEmpty(deviceName))
 558            {
 0559                deviceName = "Network Device";
 560            }
 561
 40562            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 40563            {
 40564                DeviceId = deviceId
 40565            };
 40566            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 567            {
 40568                sessionInfo.DeviceName = deviceName;
 569            }
 570            else
 571            {
 0572                sessionInfo.DeviceName = deviceOptions.CustomName;
 0573                sessionInfo.HasCustomDeviceName = true;
 574            }
 575
 40576            return sessionInfo;
 577        }
 578
 579        private List<User> GetUsers(SessionInfo session)
 580        {
 0581            var users = new List<User>();
 582
 0583            if (session.UserId.IsEmpty())
 584            {
 0585                return users;
 586            }
 587
 0588            var user = _userManager.GetUserById(session.UserId);
 589
 0590            if (user is null)
 591            {
 0592                throw new InvalidOperationException("User not found");
 593            }
 594
 0595            users.Add(user);
 596
 0597            users.AddRange(session.AdditionalUsers
 0598                .Select(i => _userManager.GetUserById(i.UserId))
 0599                .Where(i => i is not null));
 600
 0601            return users;
 602        }
 603
 604        private void StartCheckTimers()
 605        {
 0606            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 607
 0608            if (_config.Configuration.InactiveSessionThreshold > 0)
 609            {
 0610                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 611            }
 612            else
 613            {
 0614                StopInactiveCheckTimer();
 615            }
 0616        }
 617
 618        private void StopIdleCheckTimer()
 619        {
 0620            if (_idleTimer is not null)
 621            {
 0622                _idleTimer.Dispose();
 0623                _idleTimer = null;
 624            }
 0625        }
 626
 627        private void StopInactiveCheckTimer()
 628        {
 0629            if (_inactiveTimer is not null)
 630            {
 0631                _inactiveTimer.Dispose();
 0632                _inactiveTimer = null;
 633            }
 0634        }
 635
 636        private async void CheckForIdlePlayback(object state)
 637        {
 0638            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 0639                .ToList();
 640
 0641            if (playingSessions.Count > 0)
 642            {
 0643                var idle = playingSessions
 0644                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5);
 645
 0646                foreach (var session in idle)
 647                {
 0648                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 649
 650                    try
 651                    {
 0652                        await OnPlaybackStopped(new PlaybackStopInfo
 0653                        {
 0654                            Item = session.NowPlayingItem,
 0655                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 0656                            SessionId = session.Id,
 0657                            MediaSourceId = session.PlayState?.MediaSourceId,
 0658                            PositionTicks = session.PlayState?.PositionTicks
 0659                        }).ConfigureAwait(false);
 0660                    }
 0661                    catch (Exception ex)
 662                    {
 0663                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 0664                    }
 665                }
 666            }
 667            else
 668            {
 0669                StopIdleCheckTimer();
 670            }
 0671        }
 672
 673        private async void CheckForInactiveSteams(object state)
 674        {
 0675            var inactiveSessions = Sessions.Where(i =>
 0676                    i.NowPlayingItem is not null
 0677                    && i.PlayState.IsPaused
 0678                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 679
 0680            foreach (var session in inactiveSessions)
 681            {
 0682                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 683
 684                try
 685                {
 0686                    await SendPlaystateCommand(
 0687                        session.Id,
 0688                        session.Id,
 0689                        new PlaystateRequest()
 0690                        {
 0691                            Command = PlaystateCommand.Stop,
 0692                            ControllingUserId = session.UserId.ToString(),
 0693                            SeekPositionTicks = session.PlayState?.PositionTicks
 0694                        },
 0695                        CancellationToken.None).ConfigureAwait(true);
 0696                }
 0697                catch (Exception ex)
 698                {
 0699                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 0700                }
 0701            }
 702
 0703            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 704
 0705            if (!playingSessions)
 706            {
 0707                StopInactiveCheckTimer();
 708            }
 0709        }
 710
 711        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 712        {
 0713            if (session is null)
 714            {
 0715                return null;
 716            }
 717
 0718            var item = session.FullNowPlayingItem;
 0719            if (item is not null && item.Id.Equals(itemId))
 720            {
 0721                return item;
 722            }
 723
 0724            item = _libraryManager.GetItemById(itemId);
 725
 0726            session.FullNowPlayingItem = item;
 727
 0728            return item;
 729        }
 730
 731        /// <summary>
 732        /// Resolves the item whose user data (playback position, played status) should be updated
 733        /// for a playback report. When an alternate version is played the client reports the displayed
 734        /// item as <c>ItemId</c> and the played version as <c>MediaSourceId</c>.
 735        /// </summary>
 736        /// <param name="libraryItem">The now playing (displayed) item.</param>
 737        /// <param name="mediaSourceId">The reported media source id.</param>
 738        /// <returns>The item to track progress against.</returns>
 739        private BaseItem GetProgressItem(BaseItem libraryItem, string mediaSourceId)
 740        {
 0741            if (libraryItem is Video libraryVideo
 0742                && !string.IsNullOrEmpty(mediaSourceId)
 0743                && Guid.TryParse(mediaSourceId, out var mediaSourceItemId)
 0744                && !mediaSourceItemId.Equals(libraryVideo.Id))
 745            {
 0746                var versionItem = libraryVideo.GetAlternateVersion(mediaSourceItemId);
 0747                if (versionItem is not null)
 748                {
 0749                    return versionItem;
 750                }
 751            }
 752
 0753            return libraryItem;
 754        }
 755
 756        /// <summary>
 757        /// Used to report that playback has started for an item.
 758        /// </summary>
 759        /// <param name="info">The info.</param>
 760        /// <returns>Task.</returns>
 761        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 762        public async Task OnPlaybackStart(PlaybackStartInfo info)
 763        {
 0764            CheckDisposed();
 765
 0766            ArgumentNullException.ThrowIfNull(info);
 767
 0768            var session = GetSession(info.SessionId);
 769
 0770            var libraryItem = info.ItemId.IsEmpty()
 0771                ? null
 0772                : GetNowPlayingItem(session, info.ItemId);
 773
 0774            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 775
 0776            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 777            {
 0778                ClearTranscodingInfo(session.DeviceId);
 779            }
 780
 0781            session.StartAutomaticProgress(info);
 782
 0783            var users = GetUsers(session);
 784
 0785            if (libraryItem is not null)
 786            {
 0787                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0788                foreach (var user in users)
 789                {
 0790                    OnPlaybackStart(user, progressItem);
 791                }
 792            }
 793
 0794            if (!string.IsNullOrEmpty(info.LiveStreamId))
 795            {
 0796                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 797            }
 798
 0799            var eventArgs = new PlaybackStartEventArgs
 0800            {
 0801                Item = libraryItem,
 0802                Users = users,
 0803                MediaSourceId = info.MediaSourceId,
 0804                MediaInfo = info.Item,
 0805                DeviceName = session.DeviceName,
 0806                ClientName = session.Client,
 0807                DeviceId = session.DeviceId,
 0808                Session = session,
 0809                PlaybackPositionTicks = info.PositionTicks,
 0810                PlaySessionId = info.PlaySessionId
 0811            };
 812
 0813            if (info.Item is not null)
 814            {
 0815                _logger.LogInformation(
 0816                    "User {0} started playback of '{1}' ({2} {3})",
 0817                    session.UserName,
 0818                    info.Item.Name,
 0819                    session.Client,
 0820                    session.ApplicationVersion);
 821            }
 822
 0823            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 824
 825            // Nothing to save here
 826            // Fire events to inform plugins
 0827            EventHelper.QueueEventIfNotNull(
 0828                PlaybackStart,
 0829                this,
 0830                eventArgs,
 0831                _logger);
 832
 0833            StartCheckTimers();
 0834        }
 835
 836        /// <summary>
 837        /// Called when [playback start].
 838        /// </summary>
 839        /// <param name="user">The user object.</param>
 840        /// <param name="item">The item.</param>
 841        private void OnPlaybackStart(User user, BaseItem item)
 842        {
 0843            var data = _userDataManager.GetUserData(user, item);
 844
 0845            data.PlayCount++;
 0846            data.LastPlayedDate = DateTime.UtcNow;
 847
 0848            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 849            {
 0850                data.Played = true;
 851            }
 852
 0853            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0854        }
 855
 856        /// <inheritdoc />
 857        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 858        {
 0859            return OnPlaybackProgress(info, false);
 860        }
 861
 862        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 863        {
 0864            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 865
 0866            if (!string.IsNullOrEmpty(playSessionId))
 867            {
 0868                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 869                {
 0870                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 871                    {
 0872                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 873                    }
 874
 0875                    activeSessionMappings[sessionId] = playSessionId;
 0876                    activeSessionMappings[playSessionId] = sessionId;
 877                }
 878            }
 879            else
 880            {
 0881                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 882                {
 0883                    activeSessionMappings[sessionId] = string.Empty;
 884                }
 885            }
 0886        }
 887
 888        /// <summary>
 889        /// Used to report playback progress for an item.
 890        /// </summary>
 891        /// <param name="info">The playback progress info.</param>
 892        /// <param name="isAutomated">Whether this is an automated update.</param>
 893        /// <returns>Task.</returns>
 894        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 895        {
 0896            CheckDisposed();
 897
 0898            ArgumentNullException.ThrowIfNull(info);
 899
 0900            var session = GetSession(info.SessionId, false);
 0901            if (session is null)
 902            {
 0903                return;
 904            }
 905
 0906            var libraryItem = info.ItemId.IsEmpty()
 0907                ? null
 0908                : GetNowPlayingItem(session, info.ItemId);
 909
 0910            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 911
 0912            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 913            {
 0914                ClearTranscodingInfo(session.DeviceId);
 915            }
 916
 0917            var users = GetUsers(session);
 918
 919            // only update saved user data on actual check-ins, not automated ones
 0920            if (libraryItem is not null && !isAutomated)
 921            {
 0922                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 0923                foreach (var user in users)
 924                {
 0925                    OnPlaybackProgress(user, progressItem, info);
 926                }
 927            }
 928
 0929            if (!string.IsNullOrEmpty(info.LiveStreamId))
 930            {
 0931                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 932            }
 933
 0934            var eventArgs = new PlaybackProgressEventArgs
 0935            {
 0936                Item = libraryItem,
 0937                Users = users,
 0938                PlaybackPositionTicks = session.PlayState.PositionTicks,
 0939                MediaSourceId = session.PlayState.MediaSourceId,
 0940                MediaInfo = info.Item,
 0941                DeviceName = session.DeviceName,
 0942                ClientName = session.Client,
 0943                DeviceId = session.DeviceId,
 0944                IsPaused = info.IsPaused,
 0945                PlaySessionId = info.PlaySessionId,
 0946                IsAutomated = isAutomated,
 0947                Session = session
 0948            };
 949
 0950            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 951
 0952            PlaybackProgress?.Invoke(this, eventArgs);
 953
 0954            if (!isAutomated)
 955            {
 0956                session.StartAutomaticProgress(info);
 957            }
 958
 0959            StartCheckTimers();
 0960        }
 961
 962        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 963        {
 0964            var data = _userDataManager.GetUserData(user, item);
 965
 0966            var positionTicks = info.PositionTicks;
 967
 0968            var changed = false;
 969
 0970            if (positionTicks.HasValue)
 971            {
 0972                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0973                changed = true;
 974            }
 975
 0976            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0977            if (tracksChanged)
 978            {
 0979                changed = true;
 980            }
 981
 0982            if (changed)
 983            {
 0984                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 985
 986                // A completed version marks every alternate version played and clears their resume points, so the
 987                // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 988                // only persist while nothing has been completed yet.)
 0989                if (data.Played == true && item is Video playedVideo)
 990                {
 0991                    playedVideo.PropagatePlayedState(user, true);
 992                }
 993            }
 994
 0995            if ((!user.RememberAudioSelections && info.AudioStreamIndex.HasValue)
 0996                || (!user.RememberSubtitleSelections && info.SubtitleStreamIndex.HasValue))
 997            {
 0998                _userDataManager.ResetPlaybackStreamSelections(user, item);
 999            }
 01000        }
 1001
 1002        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 1003        {
 01004            var changed = false;
 1005
 01006            if (user.RememberAudioSelections)
 1007            {
 01008                if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
 1009                {
 01010                    data.AudioStreamIndex = info.AudioStreamIndex;
 01011                    changed = true;
 1012                }
 1013            }
 1014            else
 1015            {
 01016                if (data.AudioStreamIndex.HasValue)
 1017                {
 01018                    data.AudioStreamIndex = null;
 01019                    changed = true;
 1020                }
 1021            }
 1022
 01023            if (user.RememberSubtitleSelections)
 1024            {
 01025                if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 1026                {
 01027                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 01028                    changed = true;
 1029                }
 1030            }
 1031            else
 1032            {
 01033                if (data.SubtitleStreamIndex.HasValue)
 1034                {
 01035                    data.SubtitleStreamIndex = null;
 01036                    changed = true;
 1037                }
 1038            }
 1039
 01040            return changed;
 1041        }
 1042
 1043        /// <summary>
 1044        /// Used to report that playback has ended for an item.
 1045        /// </summary>
 1046        /// <param name="info">The info.</param>
 1047        /// <returns>Task.</returns>
 1048        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1049        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1050        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1051        {
 01052            CheckDisposed();
 1053
 01054            ArgumentNullException.ThrowIfNull(info);
 1055
 01056            var session = GetSession(info.SessionId);
 1057
 01058            session.StopAutomaticProgress();
 1059
 01060            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1061            {
 1062                // Ensure live stream is cleaned up before throwing, to prevent tuner
 1063                // resource leaks when stalled clients report a negative PositionTicks.
 01064                if (!string.IsNullOrEmpty(info.LiveStreamId))
 1065                {
 01066                    await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1067                }
 1068
 01069                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1070            }
 1071
 01072            var libraryItem = info.ItemId.IsEmpty()
 01073                ? null
 01074                : GetNowPlayingItem(session, info.ItemId);
 1075
 1076            // Normalize
 01077            if (string.IsNullOrEmpty(info.MediaSourceId))
 1078            {
 01079                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1080            }
 1081
 01082            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1083            {
 01084                var current = session.NowPlayingItem;
 1085
 01086                if (current is null || !info.ItemId.Equals(current.Id))
 1087                {
 01088                    MediaSourceInfo mediaSource = null;
 1089
 01090                    if (libraryItem is IHasMediaSources)
 1091                    {
 01092                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1093                    }
 1094
 01095                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1096                }
 1097                else
 1098                {
 01099                    info.Item = current;
 1100                }
 1101            }
 1102
 01103            if (info.Item is not null)
 1104            {
 01105                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1106
 01107                _logger.LogInformation(
 01108                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 01109                    session.UserName,
 01110                    info.Item.Name,
 01111                    msString,
 01112                    session.Client,
 01113                    session.ApplicationVersion);
 1114            }
 1115
 01116            if (info.NowPlayingQueue is not null)
 1117            {
 01118                session.NowPlayingQueue = info.NowPlayingQueue;
 1119            }
 1120
 01121            session.PlaylistItemId = info.PlaylistItemId;
 1122
 01123            RemoveNowPlayingItem(session);
 1124
 01125            var users = GetUsers(session);
 01126            var playedToCompletion = false;
 1127
 01128            if (libraryItem is not null)
 1129            {
 01130                var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
 01131                foreach (var user in users)
 1132                {
 01133                    playedToCompletion = OnPlaybackStopped(user, progressItem, info.PositionTicks, info.Failed);
 1134                }
 1135            }
 1136
 01137            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1138            {
 01139                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1140            }
 1141
 01142            var eventArgs = new PlaybackStopEventArgs
 01143            {
 01144                Item = libraryItem,
 01145                Users = users,
 01146                PlaybackPositionTicks = info.PositionTicks,
 01147                PlayedToCompletion = playedToCompletion,
 01148                MediaSourceId = info.MediaSourceId,
 01149                MediaInfo = info.Item,
 01150                DeviceName = session.DeviceName,
 01151                ClientName = session.Client,
 01152                DeviceId = session.DeviceId,
 01153                Session = session,
 01154                PlaySessionId = info.PlaySessionId
 01155            };
 1156
 01157            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1158
 01159            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 01160        }
 1161
 1162        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1163        {
 01164            if (playbackFailed)
 1165            {
 01166                return false;
 1167            }
 1168
 01169            var data = _userDataManager.GetUserData(user, item);
 1170            bool playedToCompletion;
 01171            if (positionTicks.HasValue)
 1172            {
 01173                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1174            }
 1175            else
 1176            {
 1177                // If the client isn't able to report this, then we'll just have to make an assumption
 01178                data.PlayCount++;
 01179                data.Played = item.SupportsPlayedStatus;
 01180                data.PlaybackPositionTicks = 0;
 01181                playedToCompletion = true;
 1182            }
 1183
 01184            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1185
 1186            // A completed version marks every alternate version played and clears their resume points, so the
 1187            // whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
 1188            // only persist while nothing has been completed yet.)
 01189            if (data.Played == true && item is Video playedVideo)
 1190            {
 01191                playedVideo.PropagatePlayedState(user, true);
 1192            }
 1193
 01194            return playedToCompletion;
 1195        }
 1196
 1197        /// <summary>
 1198        /// Gets the session.
 1199        /// </summary>
 1200        /// <param name="sessionId">The session identifier.</param>
 1201        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1202        /// <returns>SessionInfo.</returns>
 1203        /// <exception cref="ResourceNotFoundException">
 1204        /// No session with an Id equal to <c>sessionId</c> was found
 1205        /// and <c>throwOnMissing</c> is <c>true</c>.
 1206        /// </exception>
 1207        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1208        {
 31209            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 31210            if (session is null && throwOnMissing)
 1211            {
 11212                throw new ResourceNotFoundException(
 11213                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1214            }
 1215
 21216            return session;
 1217        }
 1218
 1219        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1220        {
 1221            // Accept either device id or session id
 01222            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1223
 01224            if (session is null)
 1225            {
 01226                throw new ResourceNotFoundException(
 01227                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1228            }
 1229
 01230            return session;
 1231        }
 1232
 1233        /// <inheritdoc />
 1234        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1235        {
 161236            return new SessionInfoDto
 161237            {
 161238                PlayState = sessionInfo.PlayState,
 161239                AdditionalUsers = sessionInfo.AdditionalUsers,
 161240                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 161241                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 161242                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 161243                Id = sessionInfo.Id,
 161244                UserId = sessionInfo.UserId,
 161245                UserName = sessionInfo.UserName,
 161246                Client = sessionInfo.Client,
 161247                LastActivityDate = sessionInfo.LastActivityDate,
 161248                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 161249                LastPausedDate = sessionInfo.LastPausedDate,
 161250                DeviceName = sessionInfo.DeviceName,
 161251                DeviceType = sessionInfo.DeviceType,
 161252                NowPlayingItem = sessionInfo.NowPlayingItem,
 161253                NowViewingItem = sessionInfo.NowViewingItem,
 161254                DeviceId = sessionInfo.DeviceId,
 161255                ApplicationVersion = sessionInfo.ApplicationVersion,
 161256                TranscodingInfo = sessionInfo.TranscodingInfo,
 161257                IsActive = sessionInfo.IsActive,
 161258                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 161259                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 161260                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 161261                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 161262                PlaylistItemId = sessionInfo.PlaylistItemId,
 161263                ServerId = sessionInfo.ServerId,
 161264                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 161265                SupportedCommands = sessionInfo.SupportedCommands
 161266            };
 1267        }
 1268
 1269        /// <inheritdoc />
 1270        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1271        {
 01272            CheckDisposed();
 1273
 01274            var generalCommand = new GeneralCommand
 01275            {
 01276                Name = GeneralCommandType.DisplayMessage
 01277            };
 1278
 01279            generalCommand.Arguments["Header"] = command.Header;
 01280            generalCommand.Arguments["Text"] = command.Text;
 1281
 01282            if (command.TimeoutMs.HasValue)
 1283            {
 01284                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1285            }
 1286
 01287            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1288        }
 1289
 1290        /// <inheritdoc />
 1291        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1292        {
 01293            CheckDisposed();
 1294
 01295            var session = GetSessionToRemoteControl(sessionId);
 1296
 01297            if (!string.IsNullOrEmpty(controllingSessionId))
 1298            {
 01299                var controllingSession = GetSession(controllingSessionId);
 01300                AssertCanControl(session, controllingSession);
 1301            }
 1302
 01303            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1304        }
 1305
 1306        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1307        {
 21308            var controllers = session.SessionControllers;
 21309            var messageId = Guid.NewGuid();
 1310
 81311            foreach (var controller in controllers)
 1312            {
 21313                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1314            }
 21315        }
 1316
 1317        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1318        {
 1319            IEnumerable<Task> GetTasks()
 1320            {
 1321                var messageId = Guid.NewGuid();
 1322                foreach (var session in sessions)
 1323                {
 1324                    var controllers = session.SessionControllers;
 1325                    foreach (var controller in controllers)
 1326                    {
 1327                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1328                    }
 1329                }
 1330            }
 1331
 241332            return Task.WhenAll(GetTasks());
 1333        }
 1334
 1335        /// <inheritdoc />
 1336        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1337        {
 01338            CheckDisposed();
 1339
 01340            var session = GetSessionToRemoteControl(sessionId);
 1341
 01342            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1343
 1344            List<BaseItem> items;
 1345
 01346            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1347            {
 01348                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 01349                    .ToList();
 1350
 01351                command.PlayCommand = PlayCommand.PlayNow;
 1352            }
 1353            else
 1354            {
 01355                var list = new List<BaseItem>();
 01356                foreach (var itemId in command.ItemIds)
 1357                {
 01358                    var subItems = TranslateItemForPlayback(itemId, user);
 01359                    list.AddRange(subItems);
 1360                }
 1361
 01362                items = list;
 1363            }
 1364
 01365            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1366            {
 01367                items.Shuffle();
 01368                command.PlayCommand = PlayCommand.PlayNow;
 1369            }
 1370
 01371            command.ItemIds = items.Select(i => i.Id).ToArray();
 1372
 01373            if (user is not null)
 1374            {
 01375                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1376                {
 01377                    throw new ArgumentException(
 01378                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1379                }
 1380            }
 1381
 01382            if (user is not null
 01383                && command.ItemIds.Length == 1
 01384                && user.EnableNextEpisodeAutoPlay
 01385                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1386            {
 01387                var series = episode.Series;
 01388                if (series is not null)
 1389                {
 01390                    var episodes = series.GetEpisodes(
 01391                            user,
 01392                            new DtoOptions(false)
 01393                            {
 01394                                EnableImages = false
 01395                            },
 01396                            user.DisplayMissingEpisodes)
 01397                        .Where(i => !i.IsVirtualItem)
 01398                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 01399                        .ToList();
 1400
 01401                    if (episodes.Count > 0)
 1402                    {
 01403                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1404                    }
 1405                }
 1406            }
 1407
 01408            if (!string.IsNullOrEmpty(controllingSessionId))
 1409            {
 01410                var controllingSession = GetSession(controllingSessionId);
 01411                AssertCanControl(session, controllingSession);
 01412                if (!controllingSession.UserId.IsEmpty())
 1413                {
 01414                    command.ControllingUserId = controllingSession.UserId;
 1415                }
 1416            }
 1417
 01418            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 01419        }
 1420
 1421        /// <inheritdoc />
 1422        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1423        {
 11424            CheckDisposed();
 11425            var session = GetSession(sessionId);
 11426            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 11427        }
 1428
 1429        /// <inheritdoc />
 1430        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1431        {
 21432            CheckDisposed();
 21433            var session = GetSession(sessionId);
 11434            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 11435        }
 1436
 1437        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1438        {
 01439            var item = _libraryManager.GetItemById(id);
 1440
 01441            if (item is null)
 1442            {
 01443                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01444                return Array.Empty<BaseItem>();
 1445            }
 1446
 01447            if (item is IItemByName byName)
 1448            {
 01449                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01450                {
 01451                    IsFolder = false,
 01452                    Recursive = true,
 01453                    DtoOptions = new DtoOptions(false)
 01454                    {
 01455                        EnableImages = false,
 01456                        Fields = new[]
 01457                        {
 01458                            ItemFields.SortName
 01459                        }
 01460                    },
 01461                    IsVirtualItem = false,
 01462                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01463                });
 1464            }
 1465
 01466            if (item.IsFolder)
 1467            {
 01468                var folder = (Folder)item;
 1469
 01470                return folder.GetItemList(new InternalItemsQuery(user)
 01471                {
 01472                    Recursive = true,
 01473                    IsFolder = false,
 01474                    DtoOptions = new DtoOptions(false)
 01475                    {
 01476                        EnableImages = false,
 01477                        Fields = new ItemFields[]
 01478                        {
 01479                            ItemFields.SortName
 01480                        }
 01481                    },
 01482                    IsVirtualItem = false,
 01483                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01484                });
 1485            }
 1486
 01487            return new[] { item };
 1488        }
 1489
 1490        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1491        {
 01492            var item = _libraryManager.GetItemById(id);
 1493
 01494            if (item is null)
 1495            {
 01496                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01497                return new List<BaseItem>();
 1498            }
 1499
 01500            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1501        }
 1502
 1503        /// <inheritdoc />
 1504        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1505        {
 01506            var generalCommand = new GeneralCommand
 01507            {
 01508                Name = GeneralCommandType.DisplayContent,
 01509                Arguments =
 01510                {
 01511                    ["ItemId"] = command.ItemId,
 01512                    ["ItemName"] = command.ItemName,
 01513                    ["ItemType"] = command.ItemType.ToString()
 01514                }
 01515            };
 1516
 01517            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1518        }
 1519
 1520        /// <inheritdoc />
 1521        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1522        {
 01523            CheckDisposed();
 1524
 01525            var session = GetSessionToRemoteControl(sessionId);
 1526
 01527            if (!string.IsNullOrEmpty(controllingSessionId))
 1528            {
 01529                var controllingSession = GetSession(controllingSessionId);
 01530                AssertCanControl(session, controllingSession);
 01531                if (!controllingSession.UserId.IsEmpty())
 1532                {
 01533                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1534                }
 1535            }
 1536
 01537            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1538        }
 1539
 1540        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1541        {
 01542            ArgumentNullException.ThrowIfNull(session);
 1543
 01544            ArgumentNullException.ThrowIfNull(controllingSession);
 01545        }
 1546
 1547        /// <summary>
 1548        /// Sends the restart required message.
 1549        /// </summary>
 1550        /// <param name="cancellationToken">The cancellation token.</param>
 1551        /// <returns>Task.</returns>
 1552        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1553        {
 01554            CheckDisposed();
 1555
 01556            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1557        }
 1558
 1559        /// <summary>
 1560        /// Adds the additional user.
 1561        /// </summary>
 1562        /// <param name="sessionId">The session identifier.</param>
 1563        /// <param name="userId">The user identifier.</param>
 1564        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1565        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1566        public void AddAdditionalUser(string sessionId, Guid userId)
 1567        {
 01568            CheckDisposed();
 1569
 01570            var session = GetSession(sessionId);
 1571
 01572            if (session.UserId.Equals(userId))
 1573            {
 01574                throw new ArgumentException("The requested user is already the primary user of the session.");
 1575            }
 1576
 01577            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1578            {
 01579                var user = _userManager.GetUserById(userId);
 01580                var newUser = new SessionUserInfo
 01581                {
 01582                    UserId = userId,
 01583                    UserName = user.Username
 01584                };
 1585
 01586                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1587            }
 01588        }
 1589
 1590        /// <summary>
 1591        /// Removes the additional user.
 1592        /// </summary>
 1593        /// <param name="sessionId">The session identifier.</param>
 1594        /// <param name="userId">The user identifier.</param>
 1595        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1596        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1597        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1598        {
 01599            CheckDisposed();
 1600
 01601            var session = GetSession(sessionId);
 1602
 01603            if (session.UserId.Equals(userId))
 1604            {
 01605                throw new ArgumentException("The requested user is already the primary user of the session.");
 1606            }
 1607
 01608            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1609
 01610            if (user is not null)
 1611            {
 01612                var list = session.AdditionalUsers.ToList();
 01613                list.Remove(user);
 1614
 01615                session.AdditionalUsers = list.ToArray();
 1616            }
 01617        }
 1618
 1619        /// <summary>
 1620        /// Authenticates the new session.
 1621        /// </summary>
 1622        /// <param name="request">The authenticationrequest.</param>
 1623        /// <returns>The authentication result.</returns>
 1624        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1625        {
 161626            return AuthenticateNewSessionInternal(request, true);
 1627        }
 1628
 1629        /// <summary>
 1630        /// Directly authenticates the session without enforcing password.
 1631        /// </summary>
 1632        /// <param name="request">The authentication request.</param>
 1633        /// <returns>The authentication result.</returns>
 1634        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1635        {
 01636            return AuthenticateNewSessionInternal(request, false);
 1637        }
 1638
 1639        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1640        {
 241641            CheckDisposed();
 1642
 241643            ArgumentException.ThrowIfNullOrEmpty(request.App);
 221644            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 201645            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 181646            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1647
 161648            User user = null;
 161649            if (!request.UserId.IsEmpty())
 1650            {
 01651                user = _userManager.GetUserById(request.UserId);
 1652            }
 1653
 161654            user ??= _userManager.GetUserByName(request.Username);
 1655
 161656            if (enforcePassword)
 1657            {
 161658                user = await _userManager.AuthenticateUser(
 161659                    request.Username,
 161660                    request.Password,
 161661                    request.RemoteEndPoint,
 161662                    true).ConfigureAwait(false);
 1663            }
 1664
 161665            if (user is null)
 1666            {
 01667                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 01668                throw new AuthenticationException("Invalid username or password entered.");
 1669            }
 1670
 161671            if (!string.IsNullOrEmpty(request.DeviceId)
 161672                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1673            {
 01674                throw new SecurityException("User is not allowed access from this device.");
 1675            }
 1676
 161677            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 161678            int maxActiveSessions = user.MaxActiveSessions;
 161679            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 161680            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1681            {
 01682                throw new SecurityException("User is at their maximum number of sessions.");
 1683            }
 1684
 161685            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1686
 161687            var session = await LogSessionActivity(
 161688                request.App,
 161689                request.AppVersion,
 161690                request.DeviceId,
 161691                request.DeviceName,
 161692                request.RemoteEndPoint,
 161693                user).ConfigureAwait(false);
 1694
 161695            var returnResult = new AuthenticationResult
 161696            {
 161697                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 161698                SessionInfo = ToSessionInfoDto(session),
 161699                AccessToken = token,
 161700                ServerId = _appHost.SystemId
 161701            };
 1702
 161703            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 161704            return returnResult;
 161705        }
 1706
 1707        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1708        {
 1709            // This should be validated above, but if it isn't don't delete all tokens.
 181710            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1711
 161712            var existing = _deviceManager.GetDevices(
 161713                new DeviceQuery
 161714                {
 161715                    DeviceId = deviceId,
 161716                    UserId = user.Id
 161717                }).Items;
 1718
 321719            foreach (var auth in existing)
 1720            {
 1721                try
 1722                {
 1723                    // Logout any existing sessions for the user on this device
 01724                    await Logout(auth).ConfigureAwait(false);
 01725                }
 01726                catch (Exception ex)
 1727                {
 01728                    _logger.LogError(ex, "Error while logging out existing session.");
 01729                }
 1730            }
 1731
 161732            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 161733            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1734
 161735            return device.AccessToken;
 161736        }
 1737
 1738        /// <inheritdoc />
 1739        public async Task Logout(string accessToken)
 1740        {
 01741            CheckDisposed();
 1742
 01743            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1744
 01745            var existing = _deviceManager.GetDevices(
 01746                new DeviceQuery
 01747                {
 01748                    Limit = 1,
 01749                    AccessToken = accessToken
 01750                }).Items;
 1751
 01752            if (existing.Count > 0)
 1753            {
 01754                await Logout(existing[0]).ConfigureAwait(false);
 1755            }
 01756        }
 1757
 1758        /// <inheritdoc />
 1759        public async Task Logout(Device device)
 1760        {
 01761            CheckDisposed();
 1762
 01763            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1764
 01765            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1766
 01767            var sessions = Sessions
 01768                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 01769                .ToList();
 1770
 01771            foreach (var session in sessions)
 1772            {
 1773                try
 1774                {
 01775                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 01776                }
 01777                catch (Exception ex)
 1778                {
 01779                    _logger.LogError(ex, "Error reporting session ended");
 01780                }
 1781            }
 01782        }
 1783
 1784        /// <inheritdoc />
 1785        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1786        {
 21787            CheckDisposed();
 1788
 21789            var existing = _deviceManager.GetDevices(new DeviceQuery
 21790            {
 21791                UserId = userId
 21792            });
 1793
 41794            foreach (var info in existing.Items)
 1795            {
 01796                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1797                {
 01798                    await Logout(info).ConfigureAwait(false);
 1799                }
 1800            }
 21801        }
 1802
 1803        /// <summary>
 1804        /// Reports the capabilities.
 1805        /// </summary>
 1806        /// <param name="sessionId">The session identifier.</param>
 1807        /// <param name="capabilities">The capabilities.</param>
 1808        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1809        {
 01810            CheckDisposed();
 1811
 01812            var session = GetSession(sessionId);
 1813
 01814            ReportCapabilities(session, capabilities, true);
 01815        }
 1816
 1817        private void ReportCapabilities(
 1818            SessionInfo session,
 1819            ClientCapabilities capabilities,
 1820            bool saveCapabilities)
 1821        {
 171822            session.Capabilities = capabilities;
 1823
 171824            if (saveCapabilities)
 1825            {
 01826                CapabilitiesChanged?.Invoke(
 01827                    this,
 01828                    new SessionEventArgs
 01829                    {
 01830                        SessionInfo = session
 01831                    });
 1832
 01833                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1834            }
 171835        }
 1836
 1837        /// <summary>
 1838        /// Converts a BaseItem to a BaseItemInfo.
 1839        /// </summary>
 1840        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1841        {
 01842            ArgumentNullException.ThrowIfNull(item);
 1843
 01844            var dtoOptions = _itemInfoDtoOptions;
 1845
 01846            if (_itemInfoDtoOptions is null)
 1847            {
 01848                dtoOptions = new DtoOptions
 01849                {
 01850                    AddProgramRecordingInfo = false
 01851                };
 1852
 01853                var fields = dtoOptions.Fields.ToList();
 1854
 01855                fields.Remove(ItemFields.CanDelete);
 01856                fields.Remove(ItemFields.CanDownload);
 01857                fields.Remove(ItemFields.ChildCount);
 01858                fields.Remove(ItemFields.CustomRating);
 01859                fields.Remove(ItemFields.DateLastMediaAdded);
 01860                fields.Remove(ItemFields.DateLastRefreshed);
 01861                fields.Remove(ItemFields.DateLastSaved);
 01862                fields.Remove(ItemFields.DisplayPreferencesId);
 01863                fields.Remove(ItemFields.Etag);
 01864                fields.Remove(ItemFields.ItemCounts);
 01865                fields.Remove(ItemFields.MediaSourceCount);
 01866                fields.Remove(ItemFields.MediaStreams);
 01867                fields.Remove(ItemFields.MediaSources);
 01868                fields.Remove(ItemFields.People);
 01869                fields.Remove(ItemFields.PlayAccess);
 01870                fields.Remove(ItemFields.People);
 01871                fields.Remove(ItemFields.ProductionLocations);
 01872                fields.Remove(ItemFields.RecursiveItemCount);
 01873                fields.Remove(ItemFields.RemoteTrailers);
 01874                fields.Remove(ItemFields.SeasonUserData);
 01875                fields.Remove(ItemFields.Settings);
 01876                fields.Remove(ItemFields.SortName);
 01877                fields.Remove(ItemFields.Tags);
 1878
 01879                dtoOptions.Fields = fields.ToArray();
 1880
 01881                _itemInfoDtoOptions = dtoOptions;
 1882            }
 1883
 01884            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1885
 01886            if (mediaSource is not null)
 1887            {
 01888                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1889            }
 1890
 01891            return info;
 1892        }
 1893
 1894        private string GetImageCacheTag(User user)
 1895        {
 1896            try
 1897            {
 01898                return _imageProcessor.GetImageCacheTag(user);
 1899            }
 01900            catch (Exception e)
 1901            {
 01902                _logger.LogError(e, "Error getting image information for profile image");
 01903                return null;
 1904            }
 01905        }
 1906
 1907        /// <inheritdoc />
 1908        public void ReportNowViewingItem(string sessionId, string itemId)
 1909        {
 01910            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1911
 01912            var item = _libraryManager.GetItemById(new Guid(itemId));
 01913            var session = GetSession(sessionId);
 1914
 01915            session.NowViewingItem = GetItemInfo(item, null);
 01916        }
 1917
 1918        /// <inheritdoc />
 1919        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1920        {
 01921            var session = Sessions.FirstOrDefault(i =>
 01922                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1923
 01924            if (session is not null)
 1925            {
 01926                session.TranscodingInfo = info;
 1927            }
 01928        }
 1929
 1930        /// <inheritdoc />
 1931        public void ClearTranscodingInfo(string deviceId)
 1932        {
 01933            ReportTranscodingInfo(deviceId, null);
 01934        }
 1935
 1936        /// <inheritdoc />
 1937        public SessionInfo GetSession(string deviceId, string client, string version)
 1938        {
 01939            return Sessions.FirstOrDefault(i =>
 01940                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01941                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1942        }
 1943
 1944        /// <inheritdoc />
 1945        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1946        {
 01947            ArgumentNullException.ThrowIfNull(info);
 1948
 01949            var user = info.UserId.IsEmpty()
 01950                ? null
 01951                : _userManager.GetUserById(info.UserId);
 1952
 01953            appVersion = string.IsNullOrEmpty(appVersion)
 01954                ? info.AppVersion
 01955                : appVersion;
 1956
 01957            var deviceName = info.DeviceName;
 01958            var appName = info.AppName;
 1959
 01960            if (string.IsNullOrEmpty(deviceId))
 1961            {
 01962                deviceId = info.DeviceId;
 1963            }
 1964
 1965            // Prevent argument exception
 01966            if (string.IsNullOrEmpty(appVersion))
 1967            {
 01968                appVersion = "1";
 1969            }
 1970
 01971            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1972        }
 1973
 1974        /// <inheritdoc />
 1975        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1976        {
 01977            var items = _deviceManager.GetDevices(new DeviceQuery
 01978            {
 01979                AccessToken = token,
 01980                Limit = 1
 01981            }).Items;
 1982
 01983            if (items.Count == 0)
 1984            {
 01985                return null;
 1986            }
 1987
 01988            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 01989        }
 1990
 1991        /// <inheritdoc/>
 1992        public IReadOnlyList<SessionInfoDto> GetSessions(
 1993            Guid userId,
 1994            string deviceId,
 1995            int? activeWithinSeconds,
 1996            Guid? controllableUserToCheck,
 1997            bool isApiKey)
 1998        {
 01999            var result = Sessions;
 02000            if (!string.IsNullOrEmpty(deviceId))
 2001            {
 02002                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2003            }
 2004
 02005            var userCanControlOthers = false;
 02006            var userIsAdmin = false;
 02007            User user = null;
 2008
 02009            if (isApiKey)
 2010            {
 02011                userCanControlOthers = true;
 02012                userIsAdmin = true;
 2013            }
 02014            else if (!userId.IsEmpty())
 2015            {
 02016                user = _userManager.GetUserById(userId);
 02017                if (user is not null)
 2018                {
 02019                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 02020                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 2021                }
 2022                else
 2023                {
 02024                    return [];
 2025                }
 2026            }
 2027
 02028            if (!controllableUserToCheck.IsNullOrEmpty())
 2029            {
 02030                result = result.Where(i => i.SupportsRemoteControl);
 2031
 02032                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 02033                if (controlledUser is null)
 2034                {
 02035                    return [];
 2036                }
 2037
 02038                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 2039                {
 2040                    // Controlled user has device sharing disabled
 02041                    result = result.Where(i => !i.UserId.IsEmpty());
 2042                }
 2043
 02044                if (!userCanControlOthers)
 2045                {
 2046                    // User cannot control other user's sessions, validate user id.
 02047                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2048                }
 2049
 02050                result = result.Where(i =>
 02051                {
 02052                    if (isApiKey)
 02053                    {
 02054                        return true;
 02055                    }
 02056
 02057                    if (user is null)
 02058                    {
 02059                        return false;
 02060                    }
 02061
 02062                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02063                });
 2064            }
 02065            else if (!userIsAdmin)
 2066            {
 2067                // Request isn't from administrator, limit to "own" sessions.
 02068                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2069            }
 2070
 02071            if (!userIsAdmin)
 2072            {
 2073                // Don't report acceleration type for non-admin users.
 02074                result = result.Select(r =>
 02075                {
 02076                    if (r.TranscodingInfo is not null)
 02077                    {
 02078                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02079                    }
 02080
 02081                    return r;
 02082                });
 2083            }
 2084
 02085            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2086            {
 02087                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02088                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2089            }
 2090
 02091            return result.Select(ToSessionInfoDto).ToList();
 2092        }
 2093
 2094        /// <inheritdoc />
 2095        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2096        {
 02097            CheckDisposed();
 2098
 02099            var adminUserIds = _userManager.GetUsers()
 02100                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02101                .Select(i => i.Id)
 02102                .ToList();
 2103
 02104            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2105        }
 2106
 2107        /// <inheritdoc />
 2108        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2109        {
 02110            CheckDisposed();
 2111
 02112            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2113
 02114            if (sessions.Count == 0)
 2115            {
 02116                return Task.CompletedTask;
 2117            }
 2118
 02119            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2120        }
 2121
 2122        /// <inheritdoc />
 2123        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2124        {
 22125            CheckDisposed();
 2126
 22127            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 22128            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2129        }
 2130
 2131        /// <inheritdoc />
 2132        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2133        {
 02134            CheckDisposed();
 2135
 02136            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2137
 02138            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2139        }
 2140
 2141        /// <inheritdoc />
 2142        public async ValueTask DisposeAsync()
 2143        {
 322144            if (_disposed)
 2145            {
 02146                return;
 2147            }
 2148
 642149            foreach (var session in _activeConnections.Values)
 2150            {
 02151                await session.DisposeAsync().ConfigureAwait(false);
 2152            }
 2153
 322154            if (_idleTimer is not null)
 2155            {
 02156                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 02157                _idleTimer = null;
 2158            }
 2159
 322160            if (_inactiveTimer is not null)
 2161            {
 02162                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 02163                _inactiveTimer = null;
 2164            }
 2165
 322166            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2167
 322168            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 322169            _disposed = true;
 322170        }
 2171
 2172        private async void OnApplicationStopping()
 2173        {
 222174            _logger.LogInformation("Sending shutdown notifications");
 2175            try
 2176            {
 222177                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2178
 222179                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 222180            }
 02181            catch (Exception ex)
 2182            {
 02183                _logger.LogError(ex, "Error sending server shutdown notifications");
 02184            }
 2185
 2186            // Close open websockets to allow Kestrel to shut down cleanly
 762187            foreach (var session in _activeConnections.Values)
 2188            {
 162189                await session.DisposeAsync().ConfigureAwait(false);
 2190            }
 2191
 222192            _activeConnections.Clear();
 222193            _activeLiveStreamSessions.Clear();
 222194        }
 2195    }
 2196}

Methods/Properties

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