< 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
22%
Covered lines: 112
Uncovered lines: 392
Coverable lines: 504
Total lines: 2154
Line coverage: 22.2%
Branch coverage
12%
Covered branches: 23
Total branches: 178
Branch coverage: 12.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/25/2025 - 12:09:58 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21421/29/2026 - 12:13:32 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 2154 10/25/2025 - 12:09:58 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 21421/29/2026 - 12:13:32 AM Line coverage: 22.2% (112/504) Branch coverage: 12.9% (23/178) Total lines: 2154

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Sessions()100%11100%
OnDeviceManagerDeviceOptionsUpdated(...)0%4260%
CheckDisposed()100%11100%
OnSessionStarted(...)100%44100%
UpdateDeviceName(...)0%620%
OnSessionControllerConnected(...)100%210%
GetMediaSource(...)100%210%
RemoveNowPlayingItem(...)0%620%
GetSessionKey(...)100%11100%
GetSessionInfo(...)62.5%161694.44%
CreateSessionInfo(...)57.14%141487.5%
GetUsers(...)0%2040%
StartCheckTimers()0%4260%
StopIdleCheckTimer()0%620%
StopInactiveCheckTimer()0%620%
GetNowPlayingItem(...)0%4260%
OnPlaybackStart(...)0%2040%
OnPlaybackProgress(...)100%210%
UpdateLiveStreamActiveSessionMappings(...)0%110100%
OnPlaybackProgress(...)0%4260%
UpdatePlaybackSettings(...)0%156120%
OnPlaybackStopped(...)0%2040%
GetSession(...)0%620%
GetSessionToRemoteControl(...)0%620%
ToSessionInfoDto(...)100%11100%
SendMessageCommand(...)0%620%
SendGeneralCommand(...)0%620%
SendMessageToSessions(...)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%
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%
GetSessions(...)0%600240%
SendMessageToAdminSessions(...)0%2040%
SendMessageToUserSessions(...)0%620%
SendMessageToUserSessions(...)100%210%
SendMessageToUserDeviceSessions(...)100%210%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Linq;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Queries;
 14using Jellyfin.Database.Implementations.Entities;
 15using Jellyfin.Database.Implementations.Entities.Security;
 16using Jellyfin.Database.Implementations.Enums;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Events;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Devices;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Dto;
 26using MediaBrowser.Controller.Entities;
 27using MediaBrowser.Controller.Events;
 28using MediaBrowser.Controller.Events.Authentication;
 29using MediaBrowser.Controller.Events.Session;
 30using MediaBrowser.Controller.Library;
 31using MediaBrowser.Controller.Net;
 32using MediaBrowser.Controller.Session;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.Querying;
 37using MediaBrowser.Model.Session;
 38using MediaBrowser.Model.SyncPlay;
 39using Microsoft.EntityFrameworkCore;
 40using Microsoft.Extensions.Hosting;
 41using Microsoft.Extensions.Logging;
 42using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 43
 44namespace Emby.Server.Implementations.Session
 45{
 46    /// <summary>
 47    /// Class SessionManager.
 48    /// </summary>
 49    public sealed class SessionManager : ISessionManager, IAsyncDisposable
 50    {
 51        private readonly IUserDataManager _userDataManager;
 52        private readonly IServerConfigurationManager _config;
 53        private readonly ILogger<SessionManager> _logger;
 54        private readonly IEventManager _eventManager;
 55        private readonly ILibraryManager _libraryManager;
 56        private readonly IUserManager _userManager;
 57        private readonly IMusicManager _musicManager;
 58        private readonly IDtoService _dtoService;
 59        private readonly IImageProcessor _imageProcessor;
 60        private readonly IMediaSourceManager _mediaSourceManager;
 61        private readonly IServerApplicationHost _appHost;
 62        private readonly IDeviceManager _deviceManager;
 63        private readonly CancellationTokenRegistration _shutdownCallback;
 3164        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 3165            = new(StringComparer.OrdinalIgnoreCase);
 66
 3167        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
 3168            = new(StringComparer.OrdinalIgnoreCase);
 69
 70        private Timer _idleTimer;
 71        private Timer _inactiveTimer;
 72
 73        private DtoOptions _itemInfoDtoOptions;
 74        private bool _disposed;
 75
 76        /// <summary>
 77        /// Initializes a new instance of the <see cref="SessionManager"/> class.
 78        /// </summary>
 79        /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param>
 80        /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param>
 81        /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param>
 82        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</p
 83        /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
 84        /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
 85        /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param>
 86        /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
 87        /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
 88        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
 89        /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
 90        /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
 91        /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
 92        public SessionManager(
 93            ILogger<SessionManager> logger,
 94            IEventManager eventManager,
 95            IUserDataManager userDataManager,
 96            IServerConfigurationManager serverConfigurationManager,
 97            ILibraryManager libraryManager,
 98            IUserManager userManager,
 99            IMusicManager musicManager,
 100            IDtoService dtoService,
 101            IImageProcessor imageProcessor,
 102            IServerApplicationHost appHost,
 103            IDeviceManager deviceManager,
 104            IMediaSourceManager mediaSourceManager,
 105            IHostApplicationLifetime hostApplicationLifetime)
 106        {
 31107            _logger = logger;
 31108            _eventManager = eventManager;
 31109            _userDataManager = userDataManager;
 31110            _config = serverConfigurationManager;
 31111            _libraryManager = libraryManager;
 31112            _userManager = userManager;
 31113            _musicManager = musicManager;
 31114            _dtoService = dtoService;
 31115            _imageProcessor = imageProcessor;
 31116            _appHost = appHost;
 31117            _deviceManager = deviceManager;
 31118            _mediaSourceManager = mediaSourceManager;
 31119            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 120
 31121            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 31122        }
 123
 124        /// <summary>
 125        /// Occurs when playback has started.
 126        /// </summary>
 127        public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
 128
 129        /// <summary>
 130        /// Occurs when playback has progressed.
 131        /// </summary>
 132        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
 133
 134        /// <summary>
 135        /// Occurs when playback has stopped.
 136        /// </summary>
 137        public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 138
 139        /// <inheritdoc />
 140        public event EventHandler<SessionEventArgs> SessionStarted;
 141
 142        /// <inheritdoc />
 143        public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 144
 145        /// <inheritdoc />
 146        public event EventHandler<SessionEventArgs> SessionEnded;
 147
 148        /// <inheritdoc />
 149        public event EventHandler<SessionEventArgs> SessionActivity;
 150
 151        /// <inheritdoc />
 152        public event EventHandler<SessionEventArgs> SessionControllerConnected;
 153
 154        /// <summary>
 155        /// Gets all connections.
 156        /// </summary>
 157        /// <value>All connections.</value>
 36158        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate)
 159
 160        private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e
 161        {
 0162            foreach (var session in Sessions)
 163            {
 0164                if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
 165                {
 0166                    if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
 167                    {
 0168                        session.HasCustomDeviceName = true;
 0169                        session.DeviceName = e.Argument.Item2.CustomName;
 170                    }
 171                    else
 172                    {
 0173                        session.HasCustomDeviceName = false;
 174                    }
 175                }
 176            }
 0177        }
 178
 179        private void CheckDisposed()
 180        {
 55181            ObjectDisposedException.ThrowIf(_disposed, this);
 55182        }
 183
 184        private void OnSessionStarted(SessionInfo info)
 185        {
 15186            if (!string.IsNullOrEmpty(info.DeviceId))
 187            {
 15188                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 189
 15190                if (capabilities is not null)
 191                {
 15192                    ReportCapabilities(info, capabilities, false);
 193                }
 194            }
 195
 15196            _eventManager.Publish(new SessionStartedEventArgs(info));
 197
 15198            EventHelper.QueueEventIfNotNull(
 15199                SessionStarted,
 15200                this,
 15201                new SessionEventArgs
 15202                {
 15203                    SessionInfo = info
 15204                },
 15205                _logger);
 15206        }
 207
 208        private async ValueTask OnSessionEnded(SessionInfo info)
 209        {
 210            EventHelper.QueueEventIfNotNull(
 211                SessionEnded,
 212                this,
 213                new SessionEventArgs
 214                {
 215                    SessionInfo = info
 216                },
 217                _logger);
 218
 219            _eventManager.Publish(new SessionEndedEventArgs(info));
 220
 221            await info.DisposeAsync().ConfigureAwait(false);
 222        }
 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        {
 252            CheckDisposed();
 253
 254            ArgumentException.ThrowIfNullOrEmpty(appName);
 255            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 256            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 257
 258            var activityDate = DateTime.UtcNow;
 259            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 260            var lastActivityDate = session.LastActivityDate;
 261            session.LastActivityDate = activityDate;
 262
 263            if (user is not null)
 264            {
 265                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 266
 267                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 268                {
 269                    try
 270                    {
 271                        user.LastActivityDate = activityDate;
 272                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 273                    }
 274                    catch (DbUpdateConcurrencyException e)
 275                    {
 276                        _logger.LogDebug(e, "Error updating user's last activity date.");
 277                    }
 278                }
 279            }
 280
 281            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 282            {
 283                SessionActivity?.Invoke(
 284                    this,
 285                    new SessionEventArgs
 286                    {
 287                        SessionInfo = session
 288                    });
 289            }
 290
 291            return session;
 292        }
 293
 294        /// <inheritdoc />
 295        public void OnSessionControllerConnected(SessionInfo session)
 296        {
 0297            EventHelper.QueueEventIfNotNull(
 0298                SessionControllerConnected,
 0299                this,
 0300                new SessionEventArgs
 0301                {
 0302                    SessionInfo = session
 0303                },
 0304                _logger);
 0305        }
 306
 307        /// <inheritdoc />
 308        public async Task CloseIfNeededAsync(SessionInfo session)
 309        {
 310            if (!session.SessionControllers.Any(i => i.IsSessionActive))
 311            {
 312                var key = GetSessionKey(session.Client, session.DeviceId);
 313
 314                _activeConnections.TryRemove(key, out _);
 315                if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
 316                {
 317                    await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false)
 318                }
 319
 320                await OnSessionEnded(session).ConfigureAwait(false);
 321            }
 322        }
 323
 324        /// <inheritdoc />
 325        public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
 326        {
 327            bool liveStreamNeedsToBeClosed = false;
 328
 329            if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
 330            {
 331                if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
 332                {
 333                    if (!string.IsNullOrEmpty(correspondingId))
 334                    {
 335                        activeSessionMappings.TryRemove(correspondingId, out _);
 336                    }
 337
 338                    liveStreamNeedsToBeClosed = true;
 339                }
 340
 341                if (activeSessionMappings.IsEmpty)
 342                {
 343                    _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
 344                }
 345            }
 346
 347            if (liveStreamNeedsToBeClosed)
 348            {
 349                try
 350                {
 351                    await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
 352                }
 353                catch (Exception ex)
 354                {
 355                    _logger.LogError(ex, "Error closing live stream");
 356                }
 357            }
 358        }
 359
 360        /// <inheritdoc />
 361        public async ValueTask ReportSessionEnded(string sessionId)
 362        {
 363            CheckDisposed();
 364            var session = GetSession(sessionId, false);
 365
 366            if (session is not null)
 367            {
 368                var key = GetSessionKey(session.Client, session.DeviceId);
 369
 370                _activeConnections.TryRemove(key, out _);
 371
 372                await OnSessionEnded(session).ConfigureAwait(false);
 373            }
 374        }
 375
 376        private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
 377        {
 0378            return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
 379        }
 380
 381        /// <summary>
 382        /// Updates the now playing item id.
 383        /// </summary>
 384        /// <returns>Task.</returns>
 385        private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bo
 386        {
 387            if (session is null)
 388            {
 389               return;
 390            }
 391
 392            if (string.IsNullOrEmpty(info.MediaSourceId))
 393            {
 394                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 395            }
 396
 397            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 398            {
 399                var current = session.NowPlayingItem;
 400
 401                if (current is null || !info.ItemId.Equals(current.Id))
 402                {
 403                    var runtimeTicks = libraryItem.RunTimeTicks;
 404
 405                    MediaSourceInfo mediaSource = null;
 406                    if (libraryItem is IHasMediaSources)
 407                    {
 408                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 409
 410                        if (mediaSource is not null)
 411                        {
 412                            runtimeTicks = mediaSource.RunTimeTicks;
 413                        }
 414                    }
 415
 416                    info.Item = GetItemInfo(libraryItem, mediaSource);
 417
 418                    info.Item.RunTimeTicks = runtimeTicks;
 419                }
 420                else
 421                {
 422                    info.Item = current;
 423                }
 424            }
 425
 426            session.NowPlayingItem = info.Item;
 427            session.LastActivityDate = DateTime.UtcNow;
 428
 429            if (updateLastCheckInTime)
 430            {
 431                session.LastPlaybackCheckIn = DateTime.UtcNow;
 432            }
 433
 434            if (info.IsPaused && session.LastPausedDate is null)
 435            {
 436                session.LastPausedDate = DateTime.UtcNow;
 437            }
 438            else if (!info.IsPaused)
 439            {
 440                session.LastPausedDate = null;
 441            }
 442
 443            session.PlayState.IsPaused = info.IsPaused;
 444            session.PlayState.PositionTicks = info.PositionTicks;
 445            session.PlayState.MediaSourceId = info.MediaSourceId;
 446            session.PlayState.LiveStreamId = info.LiveStreamId;
 447            session.PlayState.CanSeek = info.CanSeek;
 448            session.PlayState.IsMuted = info.IsMuted;
 449            session.PlayState.VolumeLevel = info.VolumeLevel;
 450            session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
 451            session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
 452            session.PlayState.PlayMethod = info.PlayMethod;
 453            session.PlayState.RepeatMode = info.RepeatMode;
 454            session.PlayState.PlaybackOrder = info.PlaybackOrder;
 455            session.PlaylistItemId = info.PlaylistItemId;
 456
 457            var nowPlayingQueue = info.NowPlayingQueue;
 458
 459            if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
 460            {
 461                session.NowPlayingQueue = nowPlayingQueue;
 462
 463                var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
 464                session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
 465                    _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
 466                    new DtoOptions(true));
 467            }
 468        }
 469
 470        /// <summary>
 471        /// Removes the now playing item id.
 472        /// </summary>
 473        /// <param name="session">The session.</param>
 474        private void RemoveNowPlayingItem(SessionInfo session)
 475        {
 0476            session.NowPlayingItem = null;
 0477            session.FullNowPlayingItem = null;
 0478            session.PlayState = new PlayerStateInfo();
 479
 0480            if (!string.IsNullOrEmpty(session.DeviceId))
 481            {
 0482                ClearTranscodingInfo(session.DeviceId);
 483            }
 0484        }
 485
 486        private static string GetSessionKey(string appName, string deviceId)
 15487            => appName + deviceId;
 488
 489        /// <summary>
 490        /// Gets the connection.
 491        /// </summary>
 492        /// <param name="appName">Type of the client.</param>
 493        /// <param name="appVersion">The app version.</param>
 494        /// <param name="deviceId">The device id.</param>
 495        /// <param name="deviceName">Name of the device.</param>
 496        /// <param name="remoteEndPoint">The remote end point.</param>
 497        /// <param name="user">The user.</param>
 498        /// <returns>SessionInfo.</returns>
 499        private SessionInfo GetSessionInfo(
 500            string appName,
 501            string appVersion,
 502            string deviceId,
 503            string deviceName,
 504            string remoteEndPoint,
 505            User user)
 506        {
 15507            CheckDisposed();
 508
 15509            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 510
 15511            var key = GetSessionKey(appName, deviceId);
 15512            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 15513            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 15514            if (ReferenceEquals(newSession, sessionInfo))
 515            {
 15516                OnSessionStarted(newSession);
 517            }
 518
 15519            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15520            sessionInfo.UserName = user?.Username;
 15521            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15522            sessionInfo.RemoteEndPoint = remoteEndPoint;
 15523            sessionInfo.Client = appName;
 524
 15525            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 526            {
 15527                sessionInfo.DeviceName = deviceName;
 528            }
 529
 15530            sessionInfo.ApplicationVersion = appVersion;
 531
 15532            if (user is null)
 533            {
 0534                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 535            }
 536
 15537            return sessionInfo;
 538        }
 539
 540        private SessionInfo CreateSessionInfo(
 541            string key,
 542            string appName,
 543            string appVersion,
 544            string deviceId,
 545            string deviceName,
 546            string remoteEndPoint,
 547            User user)
 548        {
 15549            var sessionInfo = new SessionInfo(this, _logger)
 15550            {
 15551                Client = appName,
 15552                DeviceId = deviceId,
 15553                ApplicationVersion = appVersion,
 15554                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 15555                ServerId = _appHost.SystemId
 15556            };
 557
 15558            var username = user?.Username;
 559
 15560            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15561            sessionInfo.UserName = username;
 15562            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15563            sessionInfo.RemoteEndPoint = remoteEndPoint;
 564
 15565            if (string.IsNullOrEmpty(deviceName))
 566            {
 0567                deviceName = "Network Device";
 568            }
 569
 15570            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 15571            {
 15572                DeviceId = deviceId
 15573            };
 15574            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 575            {
 15576                sessionInfo.DeviceName = deviceName;
 577            }
 578            else
 579            {
 0580                sessionInfo.DeviceName = deviceOptions.CustomName;
 0581                sessionInfo.HasCustomDeviceName = true;
 582            }
 583
 15584            return sessionInfo;
 585        }
 586
 587        private List<User> GetUsers(SessionInfo session)
 588        {
 0589            var users = new List<User>();
 590
 0591            if (session.UserId.IsEmpty())
 592            {
 0593                return users;
 594            }
 595
 0596            var user = _userManager.GetUserById(session.UserId);
 597
 0598            if (user is null)
 599            {
 0600                throw new InvalidOperationException("User not found");
 601            }
 602
 0603            users.Add(user);
 604
 0605            users.AddRange(session.AdditionalUsers
 0606                .Select(i => _userManager.GetUserById(i.UserId))
 0607                .Where(i => i is not null));
 608
 0609            return users;
 610        }
 611
 612        private void StartCheckTimers()
 613        {
 0614            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 615
 0616            if (_config.Configuration.InactiveSessionThreshold > 0)
 617            {
 0618                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 619            }
 620            else
 621            {
 0622                StopInactiveCheckTimer();
 623            }
 0624        }
 625
 626        private void StopIdleCheckTimer()
 627        {
 0628            if (_idleTimer is not null)
 629            {
 0630                _idleTimer.Dispose();
 0631                _idleTimer = null;
 632            }
 0633        }
 634
 635        private void StopInactiveCheckTimer()
 636        {
 0637            if (_inactiveTimer is not null)
 638            {
 0639                _inactiveTimer.Dispose();
 0640                _inactiveTimer = null;
 641            }
 0642        }
 643
 644        private async void CheckForIdlePlayback(object state)
 645        {
 646            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 647                .ToList();
 648
 649            if (playingSessions.Count > 0)
 650            {
 651                var idle = playingSessions
 652                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 653                    .ToList();
 654
 655                foreach (var session in idle)
 656                {
 657                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 658
 659                    try
 660                    {
 661                        await OnPlaybackStopped(new PlaybackStopInfo
 662                        {
 663                            Item = session.NowPlayingItem,
 664                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 665                            SessionId = session.Id,
 666                            MediaSourceId = session.PlayState?.MediaSourceId,
 667                            PositionTicks = session.PlayState?.PositionTicks
 668                        }).ConfigureAwait(false);
 669                    }
 670                    catch (Exception ex)
 671                    {
 672                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 673                    }
 674                }
 675            }
 676            else
 677            {
 678                StopIdleCheckTimer();
 679            }
 680        }
 681
 682        private async void CheckForInactiveSteams(object state)
 683        {
 684            var inactiveSessions = Sessions.Where(i =>
 685                    i.NowPlayingItem is not null
 686                    && i.PlayState.IsPaused
 687                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 688
 689            foreach (var session in inactiveSessions)
 690            {
 691                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 692
 693                try
 694                {
 695                    await SendPlaystateCommand(
 696                        session.Id,
 697                        session.Id,
 698                        new PlaystateRequest()
 699                        {
 700                            Command = PlaystateCommand.Stop,
 701                            ControllingUserId = session.UserId.ToString(),
 702                            SeekPositionTicks = session.PlayState?.PositionTicks
 703                        },
 704                        CancellationToken.None).ConfigureAwait(true);
 705                }
 706                catch (Exception ex)
 707                {
 708                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 709                }
 710            }
 711
 712            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 713
 714            if (!playingSessions)
 715            {
 716                StopInactiveCheckTimer();
 717            }
 718        }
 719
 720        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 721        {
 0722            if (session is null)
 723            {
 0724                return null;
 725            }
 726
 0727            var item = session.FullNowPlayingItem;
 0728            if (item is not null && item.Id.Equals(itemId))
 729            {
 0730                return item;
 731            }
 732
 0733            item = _libraryManager.GetItemById(itemId);
 734
 0735            session.FullNowPlayingItem = item;
 736
 0737            return item;
 738        }
 739
 740        /// <summary>
 741        /// Used to report that playback has started for an item.
 742        /// </summary>
 743        /// <param name="info">The info.</param>
 744        /// <returns>Task.</returns>
 745        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 746        public async Task OnPlaybackStart(PlaybackStartInfo info)
 747        {
 748            CheckDisposed();
 749
 750            ArgumentNullException.ThrowIfNull(info);
 751
 752            var session = GetSession(info.SessionId);
 753
 754            var libraryItem = info.ItemId.IsEmpty()
 755                ? null
 756                : GetNowPlayingItem(session, info.ItemId);
 757
 758            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 759
 760            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 761            {
 762                ClearTranscodingInfo(session.DeviceId);
 763            }
 764
 765            session.StartAutomaticProgress(info);
 766
 767            var users = GetUsers(session);
 768
 769            if (libraryItem is not null)
 770            {
 771                foreach (var user in users)
 772                {
 773                    OnPlaybackStart(user, libraryItem);
 774                }
 775            }
 776
 777            if (!string.IsNullOrEmpty(info.LiveStreamId))
 778            {
 779                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 780            }
 781
 782            var eventArgs = new PlaybackStartEventArgs
 783            {
 784                Item = libraryItem,
 785                Users = users,
 786                MediaSourceId = info.MediaSourceId,
 787                MediaInfo = info.Item,
 788                DeviceName = session.DeviceName,
 789                ClientName = session.Client,
 790                DeviceId = session.DeviceId,
 791                Session = session,
 792                PlaybackPositionTicks = info.PositionTicks,
 793                PlaySessionId = info.PlaySessionId
 794            };
 795
 796            if (info.Item is not null)
 797            {
 798                _logger.LogInformation(
 799                    "User {0} started playback of '{1}' ({2} {3})",
 800                    session.UserName,
 801                    info.Item.Name,
 802                    session.Client,
 803                    session.ApplicationVersion);
 804            }
 805
 806            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 807
 808            // Nothing to save here
 809            // Fire events to inform plugins
 810            EventHelper.QueueEventIfNotNull(
 811                PlaybackStart,
 812                this,
 813                eventArgs,
 814                _logger);
 815
 816            StartCheckTimers();
 817        }
 818
 819        /// <summary>
 820        /// Called when [playback start].
 821        /// </summary>
 822        /// <param name="user">The user object.</param>
 823        /// <param name="item">The item.</param>
 824        private void OnPlaybackStart(User user, BaseItem item)
 825        {
 0826            var data = _userDataManager.GetUserData(user, item);
 827
 0828            data.PlayCount++;
 0829            data.LastPlayedDate = DateTime.UtcNow;
 830
 0831            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 832            {
 0833                data.Played = true;
 834            }
 835            else
 836            {
 0837                data.Played = false;
 838            }
 839
 0840            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0841        }
 842
 843        /// <inheritdoc />
 844        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 845        {
 0846            return OnPlaybackProgress(info, false);
 847        }
 848
 849        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 850        {
 0851            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 852
 0853            if (!string.IsNullOrEmpty(playSessionId))
 854            {
 0855                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 856                {
 0857                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 858                    {
 0859                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 860                    }
 861
 0862                    activeSessionMappings[sessionId] = playSessionId;
 0863                    activeSessionMappings[playSessionId] = sessionId;
 864                }
 865            }
 866            else
 867            {
 0868                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 869                {
 0870                    activeSessionMappings[sessionId] = string.Empty;
 871                }
 872            }
 0873        }
 874
 875        /// <summary>
 876        /// Used to report playback progress for an item.
 877        /// </summary>
 878        /// <param name="info">The playback progress info.</param>
 879        /// <param name="isAutomated">Whether this is an automated update.</param>
 880        /// <returns>Task.</returns>
 881        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 882        {
 883            CheckDisposed();
 884
 885            ArgumentNullException.ThrowIfNull(info);
 886
 887            var session = GetSession(info.SessionId, false);
 888            if (session is null)
 889            {
 890                return;
 891            }
 892
 893            var libraryItem = info.ItemId.IsEmpty()
 894                ? null
 895                : GetNowPlayingItem(session, info.ItemId);
 896
 897            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 898
 899            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 900            {
 901                ClearTranscodingInfo(session.DeviceId);
 902            }
 903
 904            var users = GetUsers(session);
 905
 906            // only update saved user data on actual check-ins, not automated ones
 907            if (libraryItem is not null && !isAutomated)
 908            {
 909                foreach (var user in users)
 910                {
 911                    OnPlaybackProgress(user, libraryItem, info);
 912                }
 913            }
 914
 915            if (!string.IsNullOrEmpty(info.LiveStreamId))
 916            {
 917                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 918            }
 919
 920            var eventArgs = new PlaybackProgressEventArgs
 921            {
 922                Item = libraryItem,
 923                Users = users,
 924                PlaybackPositionTicks = session.PlayState.PositionTicks,
 925                MediaSourceId = session.PlayState.MediaSourceId,
 926                MediaInfo = info.Item,
 927                DeviceName = session.DeviceName,
 928                ClientName = session.Client,
 929                DeviceId = session.DeviceId,
 930                IsPaused = info.IsPaused,
 931                PlaySessionId = info.PlaySessionId,
 932                IsAutomated = isAutomated,
 933                Session = session
 934            };
 935
 936            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 937
 938            PlaybackProgress?.Invoke(this, eventArgs);
 939
 940            if (!isAutomated)
 941            {
 942                session.StartAutomaticProgress(info);
 943            }
 944
 945            StartCheckTimers();
 946        }
 947
 948        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 949        {
 0950            var data = _userDataManager.GetUserData(user, item);
 951
 0952            var positionTicks = info.PositionTicks;
 953
 0954            var changed = false;
 955
 0956            if (positionTicks.HasValue)
 957            {
 0958                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0959                changed = true;
 960            }
 961
 0962            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0963            if (!tracksChanged)
 964            {
 0965                changed = true;
 966            }
 967
 0968            if (changed)
 969            {
 0970                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 971            }
 0972        }
 973
 974        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 975        {
 0976            var changed = false;
 977
 0978            if (user.RememberAudioSelections)
 979            {
 0980                if (data.AudioStreamIndex != info.AudioStreamIndex)
 981                {
 0982                    data.AudioStreamIndex = info.AudioStreamIndex;
 0983                    changed = true;
 984                }
 985            }
 986            else
 987            {
 0988                if (data.AudioStreamIndex.HasValue)
 989                {
 0990                    data.AudioStreamIndex = null;
 0991                    changed = true;
 992                }
 993            }
 994
 0995            if (user.RememberSubtitleSelections)
 996            {
 0997                if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 998                {
 0999                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 01000                    changed = true;
 1001                }
 1002            }
 1003            else
 1004            {
 01005                if (data.SubtitleStreamIndex.HasValue)
 1006                {
 01007                    data.SubtitleStreamIndex = null;
 01008                    changed = true;
 1009                }
 1010            }
 1011
 01012            return changed;
 1013        }
 1014
 1015        /// <summary>
 1016        /// Used to report that playback has ended for an item.
 1017        /// </summary>
 1018        /// <param name="info">The info.</param>
 1019        /// <returns>Task.</returns>
 1020        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1021        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1022        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1023        {
 1024            CheckDisposed();
 1025
 1026            ArgumentNullException.ThrowIfNull(info);
 1027
 1028            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1029            {
 1030                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1031            }
 1032
 1033            var session = GetSession(info.SessionId);
 1034
 1035            session.StopAutomaticProgress();
 1036
 1037            var libraryItem = info.ItemId.IsEmpty()
 1038                ? null
 1039                : GetNowPlayingItem(session, info.ItemId);
 1040
 1041            // Normalize
 1042            if (string.IsNullOrEmpty(info.MediaSourceId))
 1043            {
 1044                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1045            }
 1046
 1047            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1048            {
 1049                var current = session.NowPlayingItem;
 1050
 1051                if (current is null || !info.ItemId.Equals(current.Id))
 1052                {
 1053                    MediaSourceInfo mediaSource = null;
 1054
 1055                    if (libraryItem is IHasMediaSources)
 1056                    {
 1057                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1058                    }
 1059
 1060                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1061                }
 1062                else
 1063                {
 1064                    info.Item = current;
 1065                }
 1066            }
 1067
 1068            if (info.Item is not null)
 1069            {
 1070                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1071
 1072                _logger.LogInformation(
 1073                    "User {0} stopped playback of '{1}' at {2}ms ({3} {4})",
 1074                    session.UserName,
 1075                    info.Item.Name,
 1076                    msString,
 1077                    session.Client,
 1078                    session.ApplicationVersion);
 1079            }
 1080
 1081            if (info.NowPlayingQueue is not null)
 1082            {
 1083                session.NowPlayingQueue = info.NowPlayingQueue;
 1084            }
 1085
 1086            session.PlaylistItemId = info.PlaylistItemId;
 1087
 1088            RemoveNowPlayingItem(session);
 1089
 1090            var users = GetUsers(session);
 1091            var playedToCompletion = false;
 1092
 1093            if (libraryItem is not null)
 1094            {
 1095                foreach (var user in users)
 1096                {
 1097                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1098                }
 1099            }
 1100
 1101            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1102            {
 1103                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1104            }
 1105
 1106            var eventArgs = new PlaybackStopEventArgs
 1107            {
 1108                Item = libraryItem,
 1109                Users = users,
 1110                PlaybackPositionTicks = info.PositionTicks,
 1111                PlayedToCompletion = playedToCompletion,
 1112                MediaSourceId = info.MediaSourceId,
 1113                MediaInfo = info.Item,
 1114                DeviceName = session.DeviceName,
 1115                ClientName = session.Client,
 1116                DeviceId = session.DeviceId,
 1117                Session = session,
 1118                PlaySessionId = info.PlaySessionId
 1119            };
 1120
 1121            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1122
 1123            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 1124        }
 1125
 1126        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1127        {
 01128            if (playbackFailed)
 1129            {
 01130                return false;
 1131            }
 1132
 01133            var data = _userDataManager.GetUserData(user, item);
 1134            bool playedToCompletion;
 01135            if (positionTicks.HasValue)
 1136            {
 01137                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1138            }
 1139            else
 1140            {
 1141                // If the client isn't able to report this, then we'll just have to make an assumption
 01142                data.PlayCount++;
 01143                data.Played = item.SupportsPlayedStatus;
 01144                data.PlaybackPositionTicks = 0;
 01145                playedToCompletion = true;
 1146            }
 1147
 01148            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1149
 01150            return playedToCompletion;
 1151        }
 1152
 1153        /// <summary>
 1154        /// Gets the session.
 1155        /// </summary>
 1156        /// <param name="sessionId">The session identifier.</param>
 1157        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1158        /// <returns>SessionInfo.</returns>
 1159        /// <exception cref="ResourceNotFoundException">
 1160        /// No session with an Id equal to <c>sessionId</c> was found
 1161        /// and <c>throwOnMissing</c> is <c>true</c>.
 1162        /// </exception>
 1163        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1164        {
 01165            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01166            if (session is null && throwOnMissing)
 1167            {
 01168                throw new ResourceNotFoundException(
 01169                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1170            }
 1171
 01172            return session;
 1173        }
 1174
 1175        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1176        {
 1177            // Accept either device id or session id
 01178            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1179
 01180            if (session is null)
 1181            {
 01182                throw new ResourceNotFoundException(
 01183                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1184            }
 1185
 01186            return session;
 1187        }
 1188
 1189        /// <inheritdoc />
 1190        public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1191        {
 151192            return new SessionInfoDto
 151193            {
 151194                PlayState = sessionInfo.PlayState,
 151195                AdditionalUsers = sessionInfo.AdditionalUsers,
 151196                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151197                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151198                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151199                Id = sessionInfo.Id,
 151200                UserId = sessionInfo.UserId,
 151201                UserName = sessionInfo.UserName,
 151202                Client = sessionInfo.Client,
 151203                LastActivityDate = sessionInfo.LastActivityDate,
 151204                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151205                LastPausedDate = sessionInfo.LastPausedDate,
 151206                DeviceName = sessionInfo.DeviceName,
 151207                DeviceType = sessionInfo.DeviceType,
 151208                NowPlayingItem = sessionInfo.NowPlayingItem,
 151209                NowViewingItem = sessionInfo.NowViewingItem,
 151210                DeviceId = sessionInfo.DeviceId,
 151211                ApplicationVersion = sessionInfo.ApplicationVersion,
 151212                TranscodingInfo = sessionInfo.TranscodingInfo,
 151213                IsActive = sessionInfo.IsActive,
 151214                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151215                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151216                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151217                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
 151218                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151219                PlaylistItemId = sessionInfo.PlaylistItemId,
 151220                ServerId = sessionInfo.ServerId,
 151221                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151222                SupportedCommands = sessionInfo.SupportedCommands
 151223            };
 1224        }
 1225
 1226        /// <inheritdoc />
 1227        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1228        {
 01229            CheckDisposed();
 1230
 01231            var generalCommand = new GeneralCommand
 01232            {
 01233                Name = GeneralCommandType.DisplayMessage
 01234            };
 1235
 01236            generalCommand.Arguments["Header"] = command.Header;
 01237            generalCommand.Arguments["Text"] = command.Text;
 1238
 01239            if (command.TimeoutMs.HasValue)
 1240            {
 01241                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1242            }
 1243
 01244            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1245        }
 1246
 1247        /// <inheritdoc />
 1248        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1249        {
 01250            CheckDisposed();
 1251
 01252            var session = GetSessionToRemoteControl(sessionId);
 1253
 01254            if (!string.IsNullOrEmpty(controllingSessionId))
 1255            {
 01256                var controllingSession = GetSession(controllingSessionId);
 01257                AssertCanControl(session, controllingSession);
 1258            }
 1259
 01260            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1261        }
 1262
 1263        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1264        {
 1265            var controllers = session.SessionControllers;
 1266            var messageId = Guid.NewGuid();
 1267
 1268            foreach (var controller in controllers)
 1269            {
 1270                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1271            }
 1272        }
 1273
 1274        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1275        {
 1276            IEnumerable<Task> GetTasks()
 1277            {
 1278                var messageId = Guid.NewGuid();
 1279                foreach (var session in sessions)
 1280                {
 1281                    var controllers = session.SessionControllers;
 1282                    foreach (var controller in controllers)
 1283                    {
 1284                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1285                    }
 1286                }
 1287            }
 1288
 211289            return Task.WhenAll(GetTasks());
 1290        }
 1291
 1292        /// <inheritdoc />
 1293        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1294        {
 1295            CheckDisposed();
 1296
 1297            var session = GetSessionToRemoteControl(sessionId);
 1298
 1299            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1300
 1301            List<BaseItem> items;
 1302
 1303            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1304            {
 1305                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 1306                    .ToList();
 1307
 1308                command.PlayCommand = PlayCommand.PlayNow;
 1309            }
 1310            else
 1311            {
 1312                var list = new List<BaseItem>();
 1313                foreach (var itemId in command.ItemIds)
 1314                {
 1315                    var subItems = TranslateItemForPlayback(itemId, user);
 1316                    list.AddRange(subItems);
 1317                }
 1318
 1319                items = list;
 1320            }
 1321
 1322            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1323            {
 1324                items.Shuffle();
 1325                command.PlayCommand = PlayCommand.PlayNow;
 1326            }
 1327
 1328            command.ItemIds = items.Select(i => i.Id).ToArray();
 1329
 1330            if (user is not null)
 1331            {
 1332                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1333                {
 1334                    throw new ArgumentException(
 1335                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1336                }
 1337            }
 1338
 1339            if (user is not null
 1340                && command.ItemIds.Length == 1
 1341                && user.EnableNextEpisodeAutoPlay
 1342                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1343            {
 1344                var series = episode.Series;
 1345                if (series is not null)
 1346                {
 1347                    var episodes = series.GetEpisodes(
 1348                            user,
 1349                            new DtoOptions(false)
 1350                            {
 1351                                EnableImages = false
 1352                            },
 1353                            user.DisplayMissingEpisodes)
 1354                        .Where(i => !i.IsVirtualItem)
 1355                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 1356                        .ToList();
 1357
 1358                    if (episodes.Count > 0)
 1359                    {
 1360                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1361                    }
 1362                }
 1363            }
 1364
 1365            if (!string.IsNullOrEmpty(controllingSessionId))
 1366            {
 1367                var controllingSession = GetSession(controllingSessionId);
 1368                AssertCanControl(session, controllingSession);
 1369                if (!controllingSession.UserId.IsEmpty())
 1370                {
 1371                    command.ControllingUserId = controllingSession.UserId;
 1372                }
 1373            }
 1374
 1375            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 1376        }
 1377
 1378        /// <inheritdoc />
 1379        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1380        {
 1381            CheckDisposed();
 1382            var session = GetSession(sessionId);
 1383            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 1384        }
 1385
 1386        /// <inheritdoc />
 1387        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1388        {
 1389            CheckDisposed();
 1390            var session = GetSession(sessionId);
 1391            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 1392        }
 1393
 1394        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1395        {
 01396            var item = _libraryManager.GetItemById(id);
 1397
 01398            if (item is null)
 1399            {
 01400                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01401                return Array.Empty<BaseItem>();
 1402            }
 1403
 01404            if (item is IItemByName byName)
 1405            {
 01406                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01407                {
 01408                    IsFolder = false,
 01409                    Recursive = true,
 01410                    DtoOptions = new DtoOptions(false)
 01411                    {
 01412                        EnableImages = false,
 01413                        Fields = new[]
 01414                        {
 01415                            ItemFields.SortName
 01416                        }
 01417                    },
 01418                    IsVirtualItem = false,
 01419                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01420                });
 1421            }
 1422
 01423            if (item.IsFolder)
 1424            {
 01425                var folder = (Folder)item;
 1426
 01427                return folder.GetItemList(new InternalItemsQuery(user)
 01428                {
 01429                    Recursive = true,
 01430                    IsFolder = false,
 01431                    DtoOptions = new DtoOptions(false)
 01432                    {
 01433                        EnableImages = false,
 01434                        Fields = new ItemFields[]
 01435                        {
 01436                            ItemFields.SortName
 01437                        }
 01438                    },
 01439                    IsVirtualItem = false,
 01440                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01441                });
 1442            }
 1443
 01444            return new[] { item };
 1445        }
 1446
 1447        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1448        {
 01449            var item = _libraryManager.GetItemById(id);
 1450
 01451            if (item is null)
 1452            {
 01453                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01454                return new List<BaseItem>();
 1455            }
 1456
 01457            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1458        }
 1459
 1460        /// <inheritdoc />
 1461        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1462        {
 01463            var generalCommand = new GeneralCommand
 01464            {
 01465                Name = GeneralCommandType.DisplayContent,
 01466                Arguments =
 01467                {
 01468                    ["ItemId"] = command.ItemId,
 01469                    ["ItemName"] = command.ItemName,
 01470                    ["ItemType"] = command.ItemType.ToString()
 01471                }
 01472            };
 1473
 01474            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1475        }
 1476
 1477        /// <inheritdoc />
 1478        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1479        {
 01480            CheckDisposed();
 1481
 01482            var session = GetSessionToRemoteControl(sessionId);
 1483
 01484            if (!string.IsNullOrEmpty(controllingSessionId))
 1485            {
 01486                var controllingSession = GetSession(controllingSessionId);
 01487                AssertCanControl(session, controllingSession);
 01488                if (!controllingSession.UserId.IsEmpty())
 1489                {
 01490                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1491                }
 1492            }
 1493
 01494            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1495        }
 1496
 1497        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1498        {
 01499            ArgumentNullException.ThrowIfNull(session);
 1500
 01501            ArgumentNullException.ThrowIfNull(controllingSession);
 01502        }
 1503
 1504        /// <summary>
 1505        /// Sends the restart required message.
 1506        /// </summary>
 1507        /// <param name="cancellationToken">The cancellation token.</param>
 1508        /// <returns>Task.</returns>
 1509        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1510        {
 01511            CheckDisposed();
 1512
 01513            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1514        }
 1515
 1516        /// <summary>
 1517        /// Adds the additional user.
 1518        /// </summary>
 1519        /// <param name="sessionId">The session identifier.</param>
 1520        /// <param name="userId">The user identifier.</param>
 1521        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1522        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1523        public void AddAdditionalUser(string sessionId, Guid userId)
 1524        {
 01525            CheckDisposed();
 1526
 01527            var session = GetSession(sessionId);
 1528
 01529            if (session.UserId.Equals(userId))
 1530            {
 01531                throw new ArgumentException("The requested user is already the primary user of the session.");
 1532            }
 1533
 01534            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1535            {
 01536                var user = _userManager.GetUserById(userId);
 01537                var newUser = new SessionUserInfo
 01538                {
 01539                    UserId = userId,
 01540                    UserName = user.Username
 01541                };
 1542
 01543                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1544            }
 01545        }
 1546
 1547        /// <summary>
 1548        /// Removes the additional user.
 1549        /// </summary>
 1550        /// <param name="sessionId">The session identifier.</param>
 1551        /// <param name="userId">The user identifier.</param>
 1552        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1553        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1554        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1555        {
 01556            CheckDisposed();
 1557
 01558            var session = GetSession(sessionId);
 1559
 01560            if (session.UserId.Equals(userId))
 1561            {
 01562                throw new ArgumentException("The requested user is already the primary user of the session.");
 1563            }
 1564
 01565            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1566
 01567            if (user is not null)
 1568            {
 01569                var list = session.AdditionalUsers.ToList();
 01570                list.Remove(user);
 1571
 01572                session.AdditionalUsers = list.ToArray();
 1573            }
 01574        }
 1575
 1576        /// <summary>
 1577        /// Authenticates the new session.
 1578        /// </summary>
 1579        /// <param name="request">The authenticationrequest.</param>
 1580        /// <returns>The authentication result.</returns>
 1581        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1582        {
 151583            return AuthenticateNewSessionInternal(request, true);
 1584        }
 1585
 1586        /// <summary>
 1587        /// Directly authenticates the session without enforcing password.
 1588        /// </summary>
 1589        /// <param name="request">The authentication request.</param>
 1590        /// <returns>The authentication result.</returns>
 1591        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1592        {
 01593            return AuthenticateNewSessionInternal(request, false);
 1594        }
 1595
 1596        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1597        {
 1598            CheckDisposed();
 1599
 1600            ArgumentException.ThrowIfNullOrEmpty(request.App);
 1601            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 1602            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 1603            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1604
 1605            User user = null;
 1606            if (!request.UserId.IsEmpty())
 1607            {
 1608                user = _userManager.GetUserById(request.UserId);
 1609            }
 1610
 1611            user ??= _userManager.GetUserByName(request.Username);
 1612
 1613            if (enforcePassword)
 1614            {
 1615                user = await _userManager.AuthenticateUser(
 1616                    request.Username,
 1617                    request.Password,
 1618                    request.RemoteEndPoint,
 1619                    true).ConfigureAwait(false);
 1620            }
 1621
 1622            if (user is null)
 1623            {
 1624                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 1625                throw new AuthenticationException("Invalid username or password entered.");
 1626            }
 1627
 1628            if (!string.IsNullOrEmpty(request.DeviceId)
 1629                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1630            {
 1631                throw new SecurityException("User is not allowed access from this device.");
 1632            }
 1633
 1634            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 1635            int maxActiveSessions = user.MaxActiveSessions;
 1636            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 1637            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1638            {
 1639                throw new SecurityException("User is at their maximum number of sessions.");
 1640            }
 1641
 1642            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1643
 1644            var session = await LogSessionActivity(
 1645                request.App,
 1646                request.AppVersion,
 1647                request.DeviceId,
 1648                request.DeviceName,
 1649                request.RemoteEndPoint,
 1650                user).ConfigureAwait(false);
 1651
 1652            var returnResult = new AuthenticationResult
 1653            {
 1654                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 1655                SessionInfo = ToSessionInfoDto(session),
 1656                AccessToken = token,
 1657                ServerId = _appHost.SystemId
 1658            };
 1659
 1660            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 1661            return returnResult;
 1662        }
 1663
 1664        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1665        {
 1666            // This should be validated above, but if it isn't don't delete all tokens.
 1667            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1668
 1669            var existing = _deviceManager.GetDevices(
 1670                new DeviceQuery
 1671                {
 1672                    DeviceId = deviceId,
 1673                    UserId = user.Id
 1674                }).Items;
 1675
 1676            foreach (var auth in existing)
 1677            {
 1678                try
 1679                {
 1680                    // Logout any existing sessions for the user on this device
 1681                    await Logout(auth).ConfigureAwait(false);
 1682                }
 1683                catch (Exception ex)
 1684                {
 1685                    _logger.LogError(ex, "Error while logging out existing session.");
 1686                }
 1687            }
 1688
 1689            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 1690            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1691
 1692            return device.AccessToken;
 1693        }
 1694
 1695        /// <inheritdoc />
 1696        public async Task Logout(string accessToken)
 1697        {
 1698            CheckDisposed();
 1699
 1700            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1701
 1702            var existing = _deviceManager.GetDevices(
 1703                new DeviceQuery
 1704                {
 1705                    Limit = 1,
 1706                    AccessToken = accessToken
 1707                }).Items;
 1708
 1709            if (existing.Count > 0)
 1710            {
 1711                await Logout(existing[0]).ConfigureAwait(false);
 1712            }
 1713        }
 1714
 1715        /// <inheritdoc />
 1716        public async Task Logout(Device device)
 1717        {
 1718            CheckDisposed();
 1719
 1720            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1721
 1722            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1723
 1724            var sessions = Sessions
 1725                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 1726                .ToList();
 1727
 1728            foreach (var session in sessions)
 1729            {
 1730                try
 1731                {
 1732                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 1733                }
 1734                catch (Exception ex)
 1735                {
 1736                    _logger.LogError(ex, "Error reporting session ended");
 1737                }
 1738            }
 1739        }
 1740
 1741        /// <inheritdoc />
 1742        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1743        {
 1744            CheckDisposed();
 1745
 1746            var existing = _deviceManager.GetDevices(new DeviceQuery
 1747            {
 1748                UserId = userId
 1749            });
 1750
 1751            foreach (var info in existing.Items)
 1752            {
 1753                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1754                {
 1755                    await Logout(info).ConfigureAwait(false);
 1756                }
 1757            }
 1758        }
 1759
 1760        /// <summary>
 1761        /// Reports the capabilities.
 1762        /// </summary>
 1763        /// <param name="sessionId">The session identifier.</param>
 1764        /// <param name="capabilities">The capabilities.</param>
 1765        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1766        {
 01767            CheckDisposed();
 1768
 01769            var session = GetSession(sessionId);
 1770
 01771            ReportCapabilities(session, capabilities, true);
 01772        }
 1773
 1774        private void ReportCapabilities(
 1775            SessionInfo session,
 1776            ClientCapabilities capabilities,
 1777            bool saveCapabilities)
 1778        {
 151779            session.Capabilities = capabilities;
 1780
 151781            if (saveCapabilities)
 1782            {
 01783                CapabilitiesChanged?.Invoke(
 01784                    this,
 01785                    new SessionEventArgs
 01786                    {
 01787                        SessionInfo = session
 01788                    });
 1789
 01790                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1791            }
 151792        }
 1793
 1794        /// <summary>
 1795        /// Converts a BaseItem to a BaseItemInfo.
 1796        /// </summary>
 1797        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1798        {
 01799            ArgumentNullException.ThrowIfNull(item);
 1800
 01801            var dtoOptions = _itemInfoDtoOptions;
 1802
 01803            if (_itemInfoDtoOptions is null)
 1804            {
 01805                dtoOptions = new DtoOptions
 01806                {
 01807                    AddProgramRecordingInfo = false
 01808                };
 1809
 01810                var fields = dtoOptions.Fields.ToList();
 1811
 01812                fields.Remove(ItemFields.CanDelete);
 01813                fields.Remove(ItemFields.CanDownload);
 01814                fields.Remove(ItemFields.ChildCount);
 01815                fields.Remove(ItemFields.CustomRating);
 01816                fields.Remove(ItemFields.DateLastMediaAdded);
 01817                fields.Remove(ItemFields.DateLastRefreshed);
 01818                fields.Remove(ItemFields.DateLastSaved);
 01819                fields.Remove(ItemFields.DisplayPreferencesId);
 01820                fields.Remove(ItemFields.Etag);
 01821                fields.Remove(ItemFields.ItemCounts);
 01822                fields.Remove(ItemFields.MediaSourceCount);
 01823                fields.Remove(ItemFields.MediaStreams);
 01824                fields.Remove(ItemFields.MediaSources);
 01825                fields.Remove(ItemFields.People);
 01826                fields.Remove(ItemFields.PlayAccess);
 01827                fields.Remove(ItemFields.People);
 01828                fields.Remove(ItemFields.ProductionLocations);
 01829                fields.Remove(ItemFields.RecursiveItemCount);
 01830                fields.Remove(ItemFields.RemoteTrailers);
 01831                fields.Remove(ItemFields.SeasonUserData);
 01832                fields.Remove(ItemFields.Settings);
 01833                fields.Remove(ItemFields.SortName);
 01834                fields.Remove(ItemFields.Tags);
 01835                fields.Remove(ItemFields.ExtraIds);
 1836
 01837                dtoOptions.Fields = fields.ToArray();
 1838
 01839                _itemInfoDtoOptions = dtoOptions;
 1840            }
 1841
 01842            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1843
 01844            if (mediaSource is not null)
 1845            {
 01846                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1847            }
 1848
 01849            return info;
 1850        }
 1851
 1852        private string GetImageCacheTag(User user)
 1853        {
 1854            try
 1855            {
 01856                return _imageProcessor.GetImageCacheTag(user);
 1857            }
 01858            catch (Exception e)
 1859            {
 01860                _logger.LogError(e, "Error getting image information for profile image");
 01861                return null;
 1862            }
 01863        }
 1864
 1865        /// <inheritdoc />
 1866        public void ReportNowViewingItem(string sessionId, string itemId)
 1867        {
 01868            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1869
 01870            var item = _libraryManager.GetItemById(new Guid(itemId));
 01871            var session = GetSession(sessionId);
 1872
 01873            session.NowViewingItem = GetItemInfo(item, null);
 01874        }
 1875
 1876        /// <inheritdoc />
 1877        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1878        {
 01879            var session = Sessions.FirstOrDefault(i =>
 01880                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1881
 01882            if (session is not null)
 1883            {
 01884                session.TranscodingInfo = info;
 1885            }
 01886        }
 1887
 1888        /// <inheritdoc />
 1889        public void ClearTranscodingInfo(string deviceId)
 1890        {
 01891            ReportTranscodingInfo(deviceId, null);
 01892        }
 1893
 1894        /// <inheritdoc />
 1895        public SessionInfo GetSession(string deviceId, string client, string version)
 1896        {
 01897            return Sessions.FirstOrDefault(i =>
 01898                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01899                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1900        }
 1901
 1902        /// <inheritdoc />
 1903        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1904        {
 01905            ArgumentNullException.ThrowIfNull(info);
 1906
 01907            var user = info.UserId.IsEmpty()
 01908                ? null
 01909                : _userManager.GetUserById(info.UserId);
 1910
 01911            appVersion = string.IsNullOrEmpty(appVersion)
 01912                ? info.AppVersion
 01913                : appVersion;
 1914
 01915            var deviceName = info.DeviceName;
 01916            var appName = info.AppName;
 1917
 01918            if (string.IsNullOrEmpty(deviceId))
 1919            {
 01920                deviceId = info.DeviceId;
 1921            }
 1922
 1923            // Prevent argument exception
 01924            if (string.IsNullOrEmpty(appVersion))
 1925            {
 01926                appVersion = "1";
 1927            }
 1928
 01929            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1930        }
 1931
 1932        /// <inheritdoc />
 1933        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1934        {
 1935            var items = _deviceManager.GetDevices(new DeviceQuery
 1936            {
 1937                AccessToken = token,
 1938                Limit = 1
 1939            }).Items;
 1940
 1941            if (items.Count == 0)
 1942            {
 1943                return null;
 1944            }
 1945
 1946            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 1947        }
 1948
 1949        /// <inheritdoc/>
 1950        public IReadOnlyList<SessionInfoDto> GetSessions(
 1951            Guid userId,
 1952            string deviceId,
 1953            int? activeWithinSeconds,
 1954            Guid? controllableUserToCheck,
 1955            bool isApiKey)
 1956        {
 01957            var result = Sessions;
 01958            if (!string.IsNullOrEmpty(deviceId))
 1959            {
 01960                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1961            }
 1962
 01963            var userCanControlOthers = false;
 01964            var userIsAdmin = false;
 01965            User user = null;
 1966
 01967            if (isApiKey)
 1968            {
 01969                userCanControlOthers = true;
 01970                userIsAdmin = true;
 1971            }
 01972            else if (!userId.IsEmpty())
 1973            {
 01974                user = _userManager.GetUserById(userId);
 01975                if (user is not null)
 1976                {
 01977                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01978                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1979                }
 1980                else
 1981                {
 01982                    return [];
 1983                }
 1984            }
 1985
 01986            if (!controllableUserToCheck.IsNullOrEmpty())
 1987            {
 01988                result = result.Where(i => i.SupportsRemoteControl);
 1989
 01990                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01991                if (controlledUser is null)
 1992                {
 01993                    return [];
 1994                }
 1995
 01996                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1997                {
 1998                    // Controlled user has device sharing disabled
 01999                    result = result.Where(i => !i.UserId.IsEmpty());
 2000                }
 2001
 02002                if (!userCanControlOthers)
 2003                {
 2004                    // User cannot control other user's sessions, validate user id.
 02005                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2006                }
 2007
 02008                result = result.Where(i =>
 02009                {
 02010                    if (isApiKey)
 02011                    {
 02012                        return true;
 02013                    }
 02014
 02015                    if (user is null)
 02016                    {
 02017                        return false;
 02018                    }
 02019
 02020                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02021                });
 2022            }
 02023            else if (!userIsAdmin)
 2024            {
 2025                // Request isn't from administrator, limit to "own" sessions.
 02026                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2027            }
 2028
 02029            if (!userIsAdmin)
 2030            {
 2031                // Don't report acceleration type for non-admin users.
 02032                result = result.Select(r =>
 02033                {
 02034                    if (r.TranscodingInfo is not null)
 02035                    {
 02036                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02037                    }
 02038
 02039                    return r;
 02040                });
 2041            }
 2042
 02043            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2044            {
 02045                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02046                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2047            }
 2048
 02049            return result.Select(ToSessionInfoDto).ToList();
 2050        }
 2051
 2052        /// <inheritdoc />
 2053        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2054        {
 02055            CheckDisposed();
 2056
 02057            var adminUserIds = _userManager.Users
 02058                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02059                .Select(i => i.Id)
 02060                .ToList();
 2061
 02062            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2063        }
 2064
 2065        /// <inheritdoc />
 2066        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2067        {
 02068            CheckDisposed();
 2069
 02070            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2071
 02072            if (sessions.Count == 0)
 2073            {
 02074                return Task.CompletedTask;
 2075            }
 2076
 02077            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2078        }
 2079
 2080        /// <inheritdoc />
 2081        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2082        {
 02083            CheckDisposed();
 2084
 02085            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 02086            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2087        }
 2088
 2089        /// <inheritdoc />
 2090        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2091        {
 02092            CheckDisposed();
 2093
 02094            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2095
 02096            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2097        }
 2098
 2099        /// <inheritdoc />
 2100        public async ValueTask DisposeAsync()
 2101        {
 2102            if (_disposed)
 2103            {
 2104                return;
 2105            }
 2106
 2107            foreach (var session in _activeConnections.Values)
 2108            {
 2109                await session.DisposeAsync().ConfigureAwait(false);
 2110            }
 2111
 2112            if (_idleTimer is not null)
 2113            {
 2114                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 2115                _idleTimer = null;
 2116            }
 2117
 2118            if (_inactiveTimer is not null)
 2119            {
 2120                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 2121                _inactiveTimer = null;
 2122            }
 2123
 2124            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2125
 2126            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 2127            _disposed = true;
 2128        }
 2129
 2130        private async void OnApplicationStopping()
 2131        {
 2132            _logger.LogInformation("Sending shutdown notifications");
 2133            try
 2134            {
 2135                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2136
 2137                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 2138            }
 2139            catch (Exception ex)
 2140            {
 2141                _logger.LogError(ex, "Error sending server shutdown notifications");
 2142            }
 2143
 2144            // Close open websockets to allow Kestrel to shut down cleanly
 2145            foreach (var session in _activeConnections.Values)
 2146            {
 2147                await session.DisposeAsync().ConfigureAwait(false);
 2148            }
 2149
 2150            _activeConnections.Clear();
 2151            _activeLiveStreamSessions.Clear();
 2152        }
 2153    }
 2154}

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)
UpdateDeviceName(System.String,System.String)
OnSessionControllerConnected(MediaBrowser.Controller.Session.SessionInfo)
GetMediaSource(MediaBrowser.Controller.Entities.BaseItem,System.String,System.String)
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()
GetNowPlayingItem(MediaBrowser.Controller.Session.SessionInfo,System.Guid)
OnPlaybackStart(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
OnPlaybackProgress(MediaBrowser.Model.Session.PlaybackProgressInfo)
UpdateLiveStreamActiveSessionMappings(System.String,System.String,System.String)
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(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)
SendMessageToSessions(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Session.SessionInfo>,MediaBrowser.Model.Session.SessionMessageType,T,System.Threading.CancellationToken)
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)
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)
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)