< 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: 391
Coverable lines: 503
Total lines: 2141
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

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)
 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.PlayState = new PlayerStateInfo();
 478
 0479            if (!string.IsNullOrEmpty(session.DeviceId))
 480            {
 0481                ClearTranscodingInfo(session.DeviceId);
 482            }
 0483        }
 484
 485        private static string GetSessionKey(string appName, string deviceId)
 15486            => appName + deviceId;
 487
 488        /// <summary>
 489        /// Gets the connection.
 490        /// </summary>
 491        /// <param name="appName">Type of the client.</param>
 492        /// <param name="appVersion">The app version.</param>
 493        /// <param name="deviceId">The device id.</param>
 494        /// <param name="deviceName">Name of the device.</param>
 495        /// <param name="remoteEndPoint">The remote end point.</param>
 496        /// <param name="user">The user.</param>
 497        /// <returns>SessionInfo.</returns>
 498        private SessionInfo GetSessionInfo(
 499            string appName,
 500            string appVersion,
 501            string deviceId,
 502            string deviceName,
 503            string remoteEndPoint,
 504            User user)
 505        {
 15506            CheckDisposed();
 507
 15508            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 509
 15510            var key = GetSessionKey(appName, deviceId);
 15511            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 15512            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 15513            if (ReferenceEquals(newSession, sessionInfo))
 514            {
 15515                OnSessionStarted(newSession);
 516            }
 517
 15518            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15519            sessionInfo.UserName = user?.Username;
 15520            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15521            sessionInfo.RemoteEndPoint = remoteEndPoint;
 15522            sessionInfo.Client = appName;
 523
 15524            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 525            {
 15526                sessionInfo.DeviceName = deviceName;
 527            }
 528
 15529            sessionInfo.ApplicationVersion = appVersion;
 530
 15531            if (user is null)
 532            {
 0533                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 534            }
 535
 15536            return sessionInfo;
 537        }
 538
 539        private SessionInfo CreateSessionInfo(
 540            string key,
 541            string appName,
 542            string appVersion,
 543            string deviceId,
 544            string deviceName,
 545            string remoteEndPoint,
 546            User user)
 547        {
 15548            var sessionInfo = new SessionInfo(this, _logger)
 15549            {
 15550                Client = appName,
 15551                DeviceId = deviceId,
 15552                ApplicationVersion = appVersion,
 15553                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 15554                ServerId = _appHost.SystemId
 15555            };
 556
 15557            var username = user?.Username;
 558
 15559            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15560            sessionInfo.UserName = username;
 15561            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15562            sessionInfo.RemoteEndPoint = remoteEndPoint;
 563
 15564            if (string.IsNullOrEmpty(deviceName))
 565            {
 0566                deviceName = "Network Device";
 567            }
 568
 15569            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 15570            {
 15571                DeviceId = deviceId
 15572            };
 15573            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 574            {
 15575                sessionInfo.DeviceName = deviceName;
 576            }
 577            else
 578            {
 0579                sessionInfo.DeviceName = deviceOptions.CustomName;
 0580                sessionInfo.HasCustomDeviceName = true;
 581            }
 582
 15583            return sessionInfo;
 584        }
 585
 586        private List<User> GetUsers(SessionInfo session)
 587        {
 0588            var users = new List<User>();
 589
 0590            if (session.UserId.IsEmpty())
 591            {
 0592                return users;
 593            }
 594
 0595            var user = _userManager.GetUserById(session.UserId);
 596
 0597            if (user is null)
 598            {
 0599                throw new InvalidOperationException("User not found");
 600            }
 601
 0602            users.Add(user);
 603
 0604            users.AddRange(session.AdditionalUsers
 0605                .Select(i => _userManager.GetUserById(i.UserId))
 0606                .Where(i => i is not null));
 607
 0608            return users;
 609        }
 610
 611        private void StartCheckTimers()
 612        {
 0613            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 614
 0615            if (_config.Configuration.InactiveSessionThreshold > 0)
 616            {
 0617                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 618            }
 619            else
 620            {
 0621                StopInactiveCheckTimer();
 622            }
 0623        }
 624
 625        private void StopIdleCheckTimer()
 626        {
 0627            if (_idleTimer is not null)
 628            {
 0629                _idleTimer.Dispose();
 0630                _idleTimer = null;
 631            }
 0632        }
 633
 634        private void StopInactiveCheckTimer()
 635        {
 0636            if (_inactiveTimer is not null)
 637            {
 0638                _inactiveTimer.Dispose();
 0639                _inactiveTimer = null;
 640            }
 0641        }
 642
 643        private async void CheckForIdlePlayback(object state)
 644        {
 645            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 646                .ToList();
 647
 648            if (playingSessions.Count > 0)
 649            {
 650                var idle = playingSessions
 651                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 652                    .ToList();
 653
 654                foreach (var session in idle)
 655                {
 656                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 657
 658                    try
 659                    {
 660                        await OnPlaybackStopped(new PlaybackStopInfo
 661                        {
 662                            Item = session.NowPlayingItem,
 663                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 664                            SessionId = session.Id,
 665                            MediaSourceId = session.PlayState?.MediaSourceId,
 666                            PositionTicks = session.PlayState?.PositionTicks
 667                        }).ConfigureAwait(false);
 668                    }
 669                    catch (Exception ex)
 670                    {
 671                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 672                    }
 673                }
 674            }
 675            else
 676            {
 677                StopIdleCheckTimer();
 678            }
 679        }
 680
 681        private async void CheckForInactiveSteams(object state)
 682        {
 683            var inactiveSessions = Sessions.Where(i =>
 684                    i.NowPlayingItem is not null
 685                    && i.PlayState.IsPaused
 686                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 687
 688            foreach (var session in inactiveSessions)
 689            {
 690                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 691
 692                try
 693                {
 694                    await SendPlaystateCommand(
 695                        session.Id,
 696                        session.Id,
 697                        new PlaystateRequest()
 698                        {
 699                            Command = PlaystateCommand.Stop,
 700                            ControllingUserId = session.UserId.ToString(),
 701                            SeekPositionTicks = session.PlayState?.PositionTicks
 702                        },
 703                        CancellationToken.None).ConfigureAwait(true);
 704                }
 705                catch (Exception ex)
 706                {
 707                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 708                }
 709            }
 710
 711            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 712
 713            if (!playingSessions)
 714            {
 715                StopInactiveCheckTimer();
 716            }
 717        }
 718
 719        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 720        {
 0721            if (session is null)
 722            {
 0723                return null;
 724            }
 725
 0726            var item = session.FullNowPlayingItem;
 0727            if (item is not null && item.Id.Equals(itemId))
 728            {
 0729                return item;
 730            }
 731
 0732            item = _libraryManager.GetItemById(itemId);
 733
 0734            session.FullNowPlayingItem = item;
 735
 0736            return item;
 737        }
 738
 739        /// <summary>
 740        /// Used to report that playback has started for an item.
 741        /// </summary>
 742        /// <param name="info">The info.</param>
 743        /// <returns>Task.</returns>
 744        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 745        public async Task OnPlaybackStart(PlaybackStartInfo info)
 746        {
 747            CheckDisposed();
 748
 749            ArgumentNullException.ThrowIfNull(info);
 750
 751            var session = GetSession(info.SessionId);
 752
 753            var libraryItem = info.ItemId.IsEmpty()
 754                ? null
 755                : GetNowPlayingItem(session, info.ItemId);
 756
 757            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 758
 759            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 760            {
 761                ClearTranscodingInfo(session.DeviceId);
 762            }
 763
 764            session.StartAutomaticProgress(info);
 765
 766            var users = GetUsers(session);
 767
 768            if (libraryItem is not null)
 769            {
 770                foreach (var user in users)
 771                {
 772                    OnPlaybackStart(user, libraryItem);
 773                }
 774            }
 775
 776            if (!string.IsNullOrEmpty(info.LiveStreamId))
 777            {
 778                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 779            }
 780
 781            var eventArgs = new PlaybackStartEventArgs
 782            {
 783                Item = libraryItem,
 784                Users = users,
 785                MediaSourceId = info.MediaSourceId,
 786                MediaInfo = info.Item,
 787                DeviceName = session.DeviceName,
 788                ClientName = session.Client,
 789                DeviceId = session.DeviceId,
 790                Session = session,
 791                PlaybackPositionTicks = info.PositionTicks,
 792                PlaySessionId = info.PlaySessionId
 793            };
 794
 795            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 796
 797            // Nothing to save here
 798            // Fire events to inform plugins
 799            EventHelper.QueueEventIfNotNull(
 800                PlaybackStart,
 801                this,
 802                eventArgs,
 803                _logger);
 804
 805            StartCheckTimers();
 806        }
 807
 808        /// <summary>
 809        /// Called when [playback start].
 810        /// </summary>
 811        /// <param name="user">The user object.</param>
 812        /// <param name="item">The item.</param>
 813        private void OnPlaybackStart(User user, BaseItem item)
 814        {
 0815            var data = _userDataManager.GetUserData(user, item);
 816
 0817            data.PlayCount++;
 0818            data.LastPlayedDate = DateTime.UtcNow;
 819
 0820            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 821            {
 0822                data.Played = true;
 823            }
 824            else
 825            {
 0826                data.Played = false;
 827            }
 828
 0829            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0830        }
 831
 832        /// <inheritdoc />
 833        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 834        {
 0835            return OnPlaybackProgress(info, false);
 836        }
 837
 838        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 839        {
 0840            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 841
 0842            if (!string.IsNullOrEmpty(playSessionId))
 843            {
 0844                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 845                {
 0846                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 847                    {
 0848                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 849                    }
 850
 0851                    activeSessionMappings[sessionId] = playSessionId;
 0852                    activeSessionMappings[playSessionId] = sessionId;
 853                }
 854            }
 855            else
 856            {
 0857                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 858                {
 0859                    activeSessionMappings[sessionId] = string.Empty;
 860                }
 861            }
 0862        }
 863
 864        /// <summary>
 865        /// Used to report playback progress for an item.
 866        /// </summary>
 867        /// <param name="info">The playback progress info.</param>
 868        /// <param name="isAutomated">Whether this is an automated update.</param>
 869        /// <returns>Task.</returns>
 870        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 871        {
 872            CheckDisposed();
 873
 874            ArgumentNullException.ThrowIfNull(info);
 875
 876            var session = GetSession(info.SessionId, false);
 877            if (session is null)
 878            {
 879                return;
 880            }
 881
 882            var libraryItem = info.ItemId.IsEmpty()
 883                ? null
 884                : GetNowPlayingItem(session, info.ItemId);
 885
 886            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 887
 888            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 889            {
 890                ClearTranscodingInfo(session.DeviceId);
 891            }
 892
 893            var users = GetUsers(session);
 894
 895            // only update saved user data on actual check-ins, not automated ones
 896            if (libraryItem is not null && !isAutomated)
 897            {
 898                foreach (var user in users)
 899                {
 900                    OnPlaybackProgress(user, libraryItem, info);
 901                }
 902            }
 903
 904            if (!string.IsNullOrEmpty(info.LiveStreamId))
 905            {
 906                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 907            }
 908
 909            var eventArgs = new PlaybackProgressEventArgs
 910            {
 911                Item = libraryItem,
 912                Users = users,
 913                PlaybackPositionTicks = session.PlayState.PositionTicks,
 914                MediaSourceId = session.PlayState.MediaSourceId,
 915                MediaInfo = info.Item,
 916                DeviceName = session.DeviceName,
 917                ClientName = session.Client,
 918                DeviceId = session.DeviceId,
 919                IsPaused = info.IsPaused,
 920                PlaySessionId = info.PlaySessionId,
 921                IsAutomated = isAutomated,
 922                Session = session
 923            };
 924
 925            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 926
 927            PlaybackProgress?.Invoke(this, eventArgs);
 928
 929            if (!isAutomated)
 930            {
 931                session.StartAutomaticProgress(info);
 932            }
 933
 934            StartCheckTimers();
 935        }
 936
 937        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 938        {
 0939            var data = _userDataManager.GetUserData(user, item);
 940
 0941            var positionTicks = info.PositionTicks;
 942
 0943            var changed = false;
 944
 0945            if (positionTicks.HasValue)
 946            {
 0947                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0948                changed = true;
 949            }
 950
 0951            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0952            if (!tracksChanged)
 953            {
 0954                changed = true;
 955            }
 956
 0957            if (changed)
 958            {
 0959                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 960            }
 0961        }
 962
 963        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 964        {
 0965            var changed = false;
 966
 0967            if (user.RememberAudioSelections)
 968            {
 0969                if (data.AudioStreamIndex != info.AudioStreamIndex)
 970                {
 0971                    data.AudioStreamIndex = info.AudioStreamIndex;
 0972                    changed = true;
 973                }
 974            }
 975            else
 976            {
 0977                if (data.AudioStreamIndex.HasValue)
 978                {
 0979                    data.AudioStreamIndex = null;
 0980                    changed = true;
 981                }
 982            }
 983
 0984            if (user.RememberSubtitleSelections)
 985            {
 0986                if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 987                {
 0988                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0989                    changed = true;
 990                }
 991            }
 992            else
 993            {
 0994                if (data.SubtitleStreamIndex.HasValue)
 995                {
 0996                    data.SubtitleStreamIndex = null;
 0997                    changed = true;
 998                }
 999            }
 1000
 01001            return changed;
 1002        }
 1003
 1004        /// <summary>
 1005        /// Used to report that playback has ended for an item.
 1006        /// </summary>
 1007        /// <param name="info">The info.</param>
 1008        /// <returns>Task.</returns>
 1009        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1010        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1011        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1012        {
 1013            CheckDisposed();
 1014
 1015            ArgumentNullException.ThrowIfNull(info);
 1016
 1017            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1018            {
 1019                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1020            }
 1021
 1022            var session = GetSession(info.SessionId);
 1023
 1024            session.StopAutomaticProgress();
 1025
 1026            var libraryItem = info.ItemId.IsEmpty()
 1027                ? null
 1028                : GetNowPlayingItem(session, info.ItemId);
 1029
 1030            // Normalize
 1031            if (string.IsNullOrEmpty(info.MediaSourceId))
 1032            {
 1033                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1034            }
 1035
 1036            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1037            {
 1038                var current = session.NowPlayingItem;
 1039
 1040                if (current is null || !info.ItemId.Equals(current.Id))
 1041                {
 1042                    MediaSourceInfo mediaSource = null;
 1043
 1044                    if (libraryItem is IHasMediaSources)
 1045                    {
 1046                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1047                    }
 1048
 1049                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1050                }
 1051                else
 1052                {
 1053                    info.Item = current;
 1054                }
 1055            }
 1056
 1057            if (info.Item is not null)
 1058            {
 1059                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1060
 1061                _logger.LogInformation(
 1062                    "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
 1063                    session.Client,
 1064                    session.ApplicationVersion,
 1065                    info.Item.Name,
 1066                    msString);
 1067            }
 1068
 1069            if (info.NowPlayingQueue is not null)
 1070            {
 1071                session.NowPlayingQueue = info.NowPlayingQueue;
 1072            }
 1073
 1074            session.PlaylistItemId = info.PlaylistItemId;
 1075
 1076            RemoveNowPlayingItem(session);
 1077
 1078            var users = GetUsers(session);
 1079            var playedToCompletion = false;
 1080
 1081            if (libraryItem is not null)
 1082            {
 1083                foreach (var user in users)
 1084                {
 1085                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1086                }
 1087            }
 1088
 1089            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1090            {
 1091                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1092            }
 1093
 1094            var eventArgs = new PlaybackStopEventArgs
 1095            {
 1096                Item = libraryItem,
 1097                Users = users,
 1098                PlaybackPositionTicks = info.PositionTicks,
 1099                PlayedToCompletion = playedToCompletion,
 1100                MediaSourceId = info.MediaSourceId,
 1101                MediaInfo = info.Item,
 1102                DeviceName = session.DeviceName,
 1103                ClientName = session.Client,
 1104                DeviceId = session.DeviceId,
 1105                Session = session,
 1106                PlaySessionId = info.PlaySessionId
 1107            };
 1108
 1109            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1110
 1111            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 1112        }
 1113
 1114        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1115        {
 01116            if (playbackFailed)
 1117            {
 01118                return false;
 1119            }
 1120
 01121            var data = _userDataManager.GetUserData(user, item);
 1122            bool playedToCompletion;
 01123            if (positionTicks.HasValue)
 1124            {
 01125                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1126            }
 1127            else
 1128            {
 1129                // If the client isn't able to report this, then we'll just have to make an assumption
 01130                data.PlayCount++;
 01131                data.Played = item.SupportsPlayedStatus;
 01132                data.PlaybackPositionTicks = 0;
 01133                playedToCompletion = true;
 1134            }
 1135
 01136            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1137
 01138            return playedToCompletion;
 1139        }
 1140
 1141        /// <summary>
 1142        /// Gets the session.
 1143        /// </summary>
 1144        /// <param name="sessionId">The session identifier.</param>
 1145        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1146        /// <returns>SessionInfo.</returns>
 1147        /// <exception cref="ResourceNotFoundException">
 1148        /// No session with an Id equal to <c>sessionId</c> was found
 1149        /// and <c>throwOnMissing</c> is <c>true</c>.
 1150        /// </exception>
 1151        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1152        {
 01153            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01154            if (session is null && throwOnMissing)
 1155            {
 01156                throw new ResourceNotFoundException(
 01157                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1158            }
 1159
 01160            return session;
 1161        }
 1162
 1163        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1164        {
 1165            // Accept either device id or session id
 01166            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1167
 01168            if (session is null)
 1169            {
 01170                throw new ResourceNotFoundException(
 01171                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1172            }
 1173
 01174            return session;
 1175        }
 1176
 1177        private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1178        {
 151179            return new SessionInfoDto
 151180            {
 151181                PlayState = sessionInfo.PlayState,
 151182                AdditionalUsers = sessionInfo.AdditionalUsers,
 151183                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151184                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151185                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151186                Id = sessionInfo.Id,
 151187                UserId = sessionInfo.UserId,
 151188                UserName = sessionInfo.UserName,
 151189                Client = sessionInfo.Client,
 151190                LastActivityDate = sessionInfo.LastActivityDate,
 151191                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151192                LastPausedDate = sessionInfo.LastPausedDate,
 151193                DeviceName = sessionInfo.DeviceName,
 151194                DeviceType = sessionInfo.DeviceType,
 151195                NowPlayingItem = sessionInfo.NowPlayingItem,
 151196                NowViewingItem = sessionInfo.NowViewingItem,
 151197                DeviceId = sessionInfo.DeviceId,
 151198                ApplicationVersion = sessionInfo.ApplicationVersion,
 151199                TranscodingInfo = sessionInfo.TranscodingInfo,
 151200                IsActive = sessionInfo.IsActive,
 151201                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151202                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151203                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151204                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
 151205                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151206                PlaylistItemId = sessionInfo.PlaylistItemId,
 151207                ServerId = sessionInfo.ServerId,
 151208                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151209                SupportedCommands = sessionInfo.SupportedCommands
 151210            };
 1211        }
 1212
 1213        /// <inheritdoc />
 1214        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1215        {
 01216            CheckDisposed();
 1217
 01218            var generalCommand = new GeneralCommand
 01219            {
 01220                Name = GeneralCommandType.DisplayMessage
 01221            };
 1222
 01223            generalCommand.Arguments["Header"] = command.Header;
 01224            generalCommand.Arguments["Text"] = command.Text;
 1225
 01226            if (command.TimeoutMs.HasValue)
 1227            {
 01228                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1229            }
 1230
 01231            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1232        }
 1233
 1234        /// <inheritdoc />
 1235        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1236        {
 01237            CheckDisposed();
 1238
 01239            var session = GetSessionToRemoteControl(sessionId);
 1240
 01241            if (!string.IsNullOrEmpty(controllingSessionId))
 1242            {
 01243                var controllingSession = GetSession(controllingSessionId);
 01244                AssertCanControl(session, controllingSession);
 1245            }
 1246
 01247            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1248        }
 1249
 1250        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1251        {
 1252            var controllers = session.SessionControllers;
 1253            var messageId = Guid.NewGuid();
 1254
 1255            foreach (var controller in controllers)
 1256            {
 1257                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1258            }
 1259        }
 1260
 1261        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1262        {
 1263            IEnumerable<Task> GetTasks()
 1264            {
 1265                var messageId = Guid.NewGuid();
 1266                foreach (var session in sessions)
 1267                {
 1268                    var controllers = session.SessionControllers;
 1269                    foreach (var controller in controllers)
 1270                    {
 1271                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1272                    }
 1273                }
 1274            }
 1275
 211276            return Task.WhenAll(GetTasks());
 1277        }
 1278
 1279        /// <inheritdoc />
 1280        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1281        {
 1282            CheckDisposed();
 1283
 1284            var session = GetSessionToRemoteControl(sessionId);
 1285
 1286            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1287
 1288            List<BaseItem> items;
 1289
 1290            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1291            {
 1292                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 1293                    .ToList();
 1294
 1295                command.PlayCommand = PlayCommand.PlayNow;
 1296            }
 1297            else
 1298            {
 1299                var list = new List<BaseItem>();
 1300                foreach (var itemId in command.ItemIds)
 1301                {
 1302                    var subItems = TranslateItemForPlayback(itemId, user);
 1303                    list.AddRange(subItems);
 1304                }
 1305
 1306                items = list;
 1307            }
 1308
 1309            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1310            {
 1311                items.Shuffle();
 1312                command.PlayCommand = PlayCommand.PlayNow;
 1313            }
 1314
 1315            command.ItemIds = items.Select(i => i.Id).ToArray();
 1316
 1317            if (user is not null)
 1318            {
 1319                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1320                {
 1321                    throw new ArgumentException(
 1322                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1323                }
 1324            }
 1325
 1326            if (user is not null
 1327                && command.ItemIds.Length == 1
 1328                && user.EnableNextEpisodeAutoPlay
 1329                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1330            {
 1331                var series = episode.Series;
 1332                if (series is not null)
 1333                {
 1334                    var episodes = series.GetEpisodes(
 1335                            user,
 1336                            new DtoOptions(false)
 1337                            {
 1338                                EnableImages = false
 1339                            },
 1340                            user.DisplayMissingEpisodes)
 1341                        .Where(i => !i.IsVirtualItem)
 1342                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 1343                        .ToList();
 1344
 1345                    if (episodes.Count > 0)
 1346                    {
 1347                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1348                    }
 1349                }
 1350            }
 1351
 1352            if (!string.IsNullOrEmpty(controllingSessionId))
 1353            {
 1354                var controllingSession = GetSession(controllingSessionId);
 1355                AssertCanControl(session, controllingSession);
 1356                if (!controllingSession.UserId.IsEmpty())
 1357                {
 1358                    command.ControllingUserId = controllingSession.UserId;
 1359                }
 1360            }
 1361
 1362            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 1363        }
 1364
 1365        /// <inheritdoc />
 1366        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1367        {
 1368            CheckDisposed();
 1369            var session = GetSession(sessionId);
 1370            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 1371        }
 1372
 1373        /// <inheritdoc />
 1374        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1375        {
 1376            CheckDisposed();
 1377            var session = GetSession(sessionId);
 1378            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 1379        }
 1380
 1381        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1382        {
 01383            var item = _libraryManager.GetItemById(id);
 1384
 01385            if (item is null)
 1386            {
 01387                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01388                return Array.Empty<BaseItem>();
 1389            }
 1390
 01391            if (item is IItemByName byName)
 1392            {
 01393                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01394                {
 01395                    IsFolder = false,
 01396                    Recursive = true,
 01397                    DtoOptions = new DtoOptions(false)
 01398                    {
 01399                        EnableImages = false,
 01400                        Fields = new[]
 01401                        {
 01402                            ItemFields.SortName
 01403                        }
 01404                    },
 01405                    IsVirtualItem = false,
 01406                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01407                });
 1408            }
 1409
 01410            if (item.IsFolder)
 1411            {
 01412                var folder = (Folder)item;
 1413
 01414                return folder.GetItemList(new InternalItemsQuery(user)
 01415                {
 01416                    Recursive = true,
 01417                    IsFolder = false,
 01418                    DtoOptions = new DtoOptions(false)
 01419                    {
 01420                        EnableImages = false,
 01421                        Fields = new ItemFields[]
 01422                        {
 01423                            ItemFields.SortName
 01424                        }
 01425                    },
 01426                    IsVirtualItem = false,
 01427                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01428                });
 1429            }
 1430
 01431            return new[] { item };
 1432        }
 1433
 1434        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1435        {
 01436            var item = _libraryManager.GetItemById(id);
 1437
 01438            if (item is null)
 1439            {
 01440                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01441                return new List<BaseItem>();
 1442            }
 1443
 01444            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1445        }
 1446
 1447        /// <inheritdoc />
 1448        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1449        {
 01450            var generalCommand = new GeneralCommand
 01451            {
 01452                Name = GeneralCommandType.DisplayContent,
 01453                Arguments =
 01454                {
 01455                    ["ItemId"] = command.ItemId,
 01456                    ["ItemName"] = command.ItemName,
 01457                    ["ItemType"] = command.ItemType.ToString()
 01458                }
 01459            };
 1460
 01461            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1462        }
 1463
 1464        /// <inheritdoc />
 1465        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1466        {
 01467            CheckDisposed();
 1468
 01469            var session = GetSessionToRemoteControl(sessionId);
 1470
 01471            if (!string.IsNullOrEmpty(controllingSessionId))
 1472            {
 01473                var controllingSession = GetSession(controllingSessionId);
 01474                AssertCanControl(session, controllingSession);
 01475                if (!controllingSession.UserId.IsEmpty())
 1476                {
 01477                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1478                }
 1479            }
 1480
 01481            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1482        }
 1483
 1484        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1485        {
 01486            ArgumentNullException.ThrowIfNull(session);
 1487
 01488            ArgumentNullException.ThrowIfNull(controllingSession);
 01489        }
 1490
 1491        /// <summary>
 1492        /// Sends the restart required message.
 1493        /// </summary>
 1494        /// <param name="cancellationToken">The cancellation token.</param>
 1495        /// <returns>Task.</returns>
 1496        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1497        {
 01498            CheckDisposed();
 1499
 01500            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1501        }
 1502
 1503        /// <summary>
 1504        /// Adds the additional user.
 1505        /// </summary>
 1506        /// <param name="sessionId">The session identifier.</param>
 1507        /// <param name="userId">The user identifier.</param>
 1508        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1509        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1510        public void AddAdditionalUser(string sessionId, Guid userId)
 1511        {
 01512            CheckDisposed();
 1513
 01514            var session = GetSession(sessionId);
 1515
 01516            if (session.UserId.Equals(userId))
 1517            {
 01518                throw new ArgumentException("The requested user is already the primary user of the session.");
 1519            }
 1520
 01521            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1522            {
 01523                var user = _userManager.GetUserById(userId);
 01524                var newUser = new SessionUserInfo
 01525                {
 01526                    UserId = userId,
 01527                    UserName = user.Username
 01528                };
 1529
 01530                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1531            }
 01532        }
 1533
 1534        /// <summary>
 1535        /// Removes the additional user.
 1536        /// </summary>
 1537        /// <param name="sessionId">The session identifier.</param>
 1538        /// <param name="userId">The user identifier.</param>
 1539        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1540        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1541        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1542        {
 01543            CheckDisposed();
 1544
 01545            var session = GetSession(sessionId);
 1546
 01547            if (session.UserId.Equals(userId))
 1548            {
 01549                throw new ArgumentException("The requested user is already the primary user of the session.");
 1550            }
 1551
 01552            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1553
 01554            if (user is not null)
 1555            {
 01556                var list = session.AdditionalUsers.ToList();
 01557                list.Remove(user);
 1558
 01559                session.AdditionalUsers = list.ToArray();
 1560            }
 01561        }
 1562
 1563        /// <summary>
 1564        /// Authenticates the new session.
 1565        /// </summary>
 1566        /// <param name="request">The authenticationrequest.</param>
 1567        /// <returns>The authentication result.</returns>
 1568        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1569        {
 151570            return AuthenticateNewSessionInternal(request, true);
 1571        }
 1572
 1573        /// <summary>
 1574        /// Directly authenticates the session without enforcing password.
 1575        /// </summary>
 1576        /// <param name="request">The authentication request.</param>
 1577        /// <returns>The authentication result.</returns>
 1578        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1579        {
 01580            return AuthenticateNewSessionInternal(request, false);
 1581        }
 1582
 1583        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1584        {
 1585            CheckDisposed();
 1586
 1587            ArgumentException.ThrowIfNullOrEmpty(request.App);
 1588            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 1589            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 1590            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1591
 1592            User user = null;
 1593            if (!request.UserId.IsEmpty())
 1594            {
 1595                user = _userManager.GetUserById(request.UserId);
 1596            }
 1597
 1598            user ??= _userManager.GetUserByName(request.Username);
 1599
 1600            if (enforcePassword)
 1601            {
 1602                user = await _userManager.AuthenticateUser(
 1603                    request.Username,
 1604                    request.Password,
 1605                    request.RemoteEndPoint,
 1606                    true).ConfigureAwait(false);
 1607            }
 1608
 1609            if (user is null)
 1610            {
 1611                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 1612                throw new AuthenticationException("Invalid username or password entered.");
 1613            }
 1614
 1615            if (!string.IsNullOrEmpty(request.DeviceId)
 1616                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1617            {
 1618                throw new SecurityException("User is not allowed access from this device.");
 1619            }
 1620
 1621            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 1622            int maxActiveSessions = user.MaxActiveSessions;
 1623            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 1624            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1625            {
 1626                throw new SecurityException("User is at their maximum number of sessions.");
 1627            }
 1628
 1629            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1630
 1631            var session = await LogSessionActivity(
 1632                request.App,
 1633                request.AppVersion,
 1634                request.DeviceId,
 1635                request.DeviceName,
 1636                request.RemoteEndPoint,
 1637                user).ConfigureAwait(false);
 1638
 1639            var returnResult = new AuthenticationResult
 1640            {
 1641                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 1642                SessionInfo = ToSessionInfoDto(session),
 1643                AccessToken = token,
 1644                ServerId = _appHost.SystemId
 1645            };
 1646
 1647            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 1648            return returnResult;
 1649        }
 1650
 1651        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1652        {
 1653            // This should be validated above, but if it isn't don't delete all tokens.
 1654            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1655
 1656            var existing = _deviceManager.GetDevices(
 1657                new DeviceQuery
 1658                {
 1659                    DeviceId = deviceId,
 1660                    UserId = user.Id
 1661                }).Items;
 1662
 1663            foreach (var auth in existing)
 1664            {
 1665                try
 1666                {
 1667                    // Logout any existing sessions for the user on this device
 1668                    await Logout(auth).ConfigureAwait(false);
 1669                }
 1670                catch (Exception ex)
 1671                {
 1672                    _logger.LogError(ex, "Error while logging out existing session.");
 1673                }
 1674            }
 1675
 1676            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 1677            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1678
 1679            return device.AccessToken;
 1680        }
 1681
 1682        /// <inheritdoc />
 1683        public async Task Logout(string accessToken)
 1684        {
 1685            CheckDisposed();
 1686
 1687            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1688
 1689            var existing = _deviceManager.GetDevices(
 1690                new DeviceQuery
 1691                {
 1692                    Limit = 1,
 1693                    AccessToken = accessToken
 1694                }).Items;
 1695
 1696            if (existing.Count > 0)
 1697            {
 1698                await Logout(existing[0]).ConfigureAwait(false);
 1699            }
 1700        }
 1701
 1702        /// <inheritdoc />
 1703        public async Task Logout(Device device)
 1704        {
 1705            CheckDisposed();
 1706
 1707            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1708
 1709            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1710
 1711            var sessions = Sessions
 1712                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 1713                .ToList();
 1714
 1715            foreach (var session in sessions)
 1716            {
 1717                try
 1718                {
 1719                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 1720                }
 1721                catch (Exception ex)
 1722                {
 1723                    _logger.LogError(ex, "Error reporting session ended");
 1724                }
 1725            }
 1726        }
 1727
 1728        /// <inheritdoc />
 1729        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1730        {
 1731            CheckDisposed();
 1732
 1733            var existing = _deviceManager.GetDevices(new DeviceQuery
 1734            {
 1735                UserId = userId
 1736            });
 1737
 1738            foreach (var info in existing.Items)
 1739            {
 1740                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1741                {
 1742                    await Logout(info).ConfigureAwait(false);
 1743                }
 1744            }
 1745        }
 1746
 1747        /// <summary>
 1748        /// Reports the capabilities.
 1749        /// </summary>
 1750        /// <param name="sessionId">The session identifier.</param>
 1751        /// <param name="capabilities">The capabilities.</param>
 1752        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1753        {
 01754            CheckDisposed();
 1755
 01756            var session = GetSession(sessionId);
 1757
 01758            ReportCapabilities(session, capabilities, true);
 01759        }
 1760
 1761        private void ReportCapabilities(
 1762            SessionInfo session,
 1763            ClientCapabilities capabilities,
 1764            bool saveCapabilities)
 1765        {
 151766            session.Capabilities = capabilities;
 1767
 151768            if (saveCapabilities)
 1769            {
 01770                CapabilitiesChanged?.Invoke(
 01771                    this,
 01772                    new SessionEventArgs
 01773                    {
 01774                        SessionInfo = session
 01775                    });
 1776
 01777                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1778            }
 151779        }
 1780
 1781        /// <summary>
 1782        /// Converts a BaseItem to a BaseItemInfo.
 1783        /// </summary>
 1784        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1785        {
 01786            ArgumentNullException.ThrowIfNull(item);
 1787
 01788            var dtoOptions = _itemInfoDtoOptions;
 1789
 01790            if (_itemInfoDtoOptions is null)
 1791            {
 01792                dtoOptions = new DtoOptions
 01793                {
 01794                    AddProgramRecordingInfo = false
 01795                };
 1796
 01797                var fields = dtoOptions.Fields.ToList();
 1798
 01799                fields.Remove(ItemFields.CanDelete);
 01800                fields.Remove(ItemFields.CanDownload);
 01801                fields.Remove(ItemFields.ChildCount);
 01802                fields.Remove(ItemFields.CustomRating);
 01803                fields.Remove(ItemFields.DateLastMediaAdded);
 01804                fields.Remove(ItemFields.DateLastRefreshed);
 01805                fields.Remove(ItemFields.DateLastSaved);
 01806                fields.Remove(ItemFields.DisplayPreferencesId);
 01807                fields.Remove(ItemFields.Etag);
 01808                fields.Remove(ItemFields.ItemCounts);
 01809                fields.Remove(ItemFields.MediaSourceCount);
 01810                fields.Remove(ItemFields.MediaStreams);
 01811                fields.Remove(ItemFields.MediaSources);
 01812                fields.Remove(ItemFields.People);
 01813                fields.Remove(ItemFields.PlayAccess);
 01814                fields.Remove(ItemFields.People);
 01815                fields.Remove(ItemFields.ProductionLocations);
 01816                fields.Remove(ItemFields.RecursiveItemCount);
 01817                fields.Remove(ItemFields.RemoteTrailers);
 01818                fields.Remove(ItemFields.SeasonUserData);
 01819                fields.Remove(ItemFields.Settings);
 01820                fields.Remove(ItemFields.SortName);
 01821                fields.Remove(ItemFields.Tags);
 01822                fields.Remove(ItemFields.ExtraIds);
 1823
 01824                dtoOptions.Fields = fields.ToArray();
 1825
 01826                _itemInfoDtoOptions = dtoOptions;
 1827            }
 1828
 01829            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1830
 01831            if (mediaSource is not null)
 1832            {
 01833                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1834            }
 1835
 01836            return info;
 1837        }
 1838
 1839        private string GetImageCacheTag(User user)
 1840        {
 1841            try
 1842            {
 01843                return _imageProcessor.GetImageCacheTag(user);
 1844            }
 01845            catch (Exception e)
 1846            {
 01847                _logger.LogError(e, "Error getting image information for profile image");
 01848                return null;
 1849            }
 01850        }
 1851
 1852        /// <inheritdoc />
 1853        public void ReportNowViewingItem(string sessionId, string itemId)
 1854        {
 01855            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1856
 01857            var item = _libraryManager.GetItemById(new Guid(itemId));
 01858            var session = GetSession(sessionId);
 1859
 01860            session.NowViewingItem = GetItemInfo(item, null);
 01861        }
 1862
 1863        /// <inheritdoc />
 1864        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1865        {
 01866            var session = Sessions.FirstOrDefault(i =>
 01867                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1868
 01869            if (session is not null)
 1870            {
 01871                session.TranscodingInfo = info;
 1872            }
 01873        }
 1874
 1875        /// <inheritdoc />
 1876        public void ClearTranscodingInfo(string deviceId)
 1877        {
 01878            ReportTranscodingInfo(deviceId, null);
 01879        }
 1880
 1881        /// <inheritdoc />
 1882        public SessionInfo GetSession(string deviceId, string client, string version)
 1883        {
 01884            return Sessions.FirstOrDefault(i =>
 01885                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01886                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1887        }
 1888
 1889        /// <inheritdoc />
 1890        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1891        {
 01892            ArgumentNullException.ThrowIfNull(info);
 1893
 01894            var user = info.UserId.IsEmpty()
 01895                ? null
 01896                : _userManager.GetUserById(info.UserId);
 1897
 01898            appVersion = string.IsNullOrEmpty(appVersion)
 01899                ? info.AppVersion
 01900                : appVersion;
 1901
 01902            var deviceName = info.DeviceName;
 01903            var appName = info.AppName;
 1904
 01905            if (string.IsNullOrEmpty(deviceId))
 1906            {
 01907                deviceId = info.DeviceId;
 1908            }
 1909
 1910            // Prevent argument exception
 01911            if (string.IsNullOrEmpty(appVersion))
 1912            {
 01913                appVersion = "1";
 1914            }
 1915
 01916            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1917        }
 1918
 1919        /// <inheritdoc />
 1920        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1921        {
 1922            var items = _deviceManager.GetDevices(new DeviceQuery
 1923            {
 1924                AccessToken = token,
 1925                Limit = 1
 1926            }).Items;
 1927
 1928            if (items.Count == 0)
 1929            {
 1930                return null;
 1931            }
 1932
 1933            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 1934        }
 1935
 1936        /// <inheritdoc/>
 1937        public IReadOnlyList<SessionInfoDto> GetSessions(
 1938            Guid userId,
 1939            string deviceId,
 1940            int? activeWithinSeconds,
 1941            Guid? controllableUserToCheck,
 1942            bool isApiKey)
 1943        {
 01944            var result = Sessions;
 01945            if (!string.IsNullOrEmpty(deviceId))
 1946            {
 01947                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1948            }
 1949
 01950            var userCanControlOthers = false;
 01951            var userIsAdmin = false;
 01952            User user = null;
 1953
 01954            if (isApiKey)
 1955            {
 01956                userCanControlOthers = true;
 01957                userIsAdmin = true;
 1958            }
 01959            else if (!userId.IsEmpty())
 1960            {
 01961                user = _userManager.GetUserById(userId);
 01962                if (user is not null)
 1963                {
 01964                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01965                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1966                }
 1967                else
 1968                {
 01969                    return [];
 1970                }
 1971            }
 1972
 01973            if (!controllableUserToCheck.IsNullOrEmpty())
 1974            {
 01975                result = result.Where(i => i.SupportsRemoteControl);
 1976
 01977                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01978                if (controlledUser is null)
 1979                {
 01980                    return [];
 1981                }
 1982
 01983                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1984                {
 1985                    // Controlled user has device sharing disabled
 01986                    result = result.Where(i => !i.UserId.IsEmpty());
 1987                }
 1988
 01989                if (!userCanControlOthers)
 1990                {
 1991                    // User cannot control other user's sessions, validate user id.
 01992                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 1993                }
 1994
 01995                result = result.Where(i =>
 01996                {
 01997                    if (isApiKey)
 01998                    {
 01999                        return true;
 02000                    }
 02001
 02002                    if (user is null)
 02003                    {
 02004                        return false;
 02005                    }
 02006
 02007                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02008                });
 2009            }
 02010            else if (!userIsAdmin)
 2011            {
 2012                // Request isn't from administrator, limit to "own" sessions.
 02013                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2014            }
 2015
 02016            if (!userIsAdmin)
 2017            {
 2018                // Don't report acceleration type for non-admin users.
 02019                result = result.Select(r =>
 02020                {
 02021                    if (r.TranscodingInfo is not null)
 02022                    {
 02023                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02024                    }
 02025
 02026                    return r;
 02027                });
 2028            }
 2029
 02030            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2031            {
 02032                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02033                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2034            }
 2035
 02036            return result.Select(ToSessionInfoDto).ToList();
 2037        }
 2038
 2039        /// <inheritdoc />
 2040        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2041        {
 02042            CheckDisposed();
 2043
 02044            var adminUserIds = _userManager.Users
 02045                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02046                .Select(i => i.Id)
 02047                .ToList();
 2048
 02049            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2050        }
 2051
 2052        /// <inheritdoc />
 2053        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2054        {
 02055            CheckDisposed();
 2056
 02057            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2058
 02059            if (sessions.Count == 0)
 2060            {
 02061                return Task.CompletedTask;
 2062            }
 2063
 02064            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2065        }
 2066
 2067        /// <inheritdoc />
 2068        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2069        {
 02070            CheckDisposed();
 2071
 02072            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 02073            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2074        }
 2075
 2076        /// <inheritdoc />
 2077        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2078        {
 02079            CheckDisposed();
 2080
 02081            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2082
 02083            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2084        }
 2085
 2086        /// <inheritdoc />
 2087        public async ValueTask DisposeAsync()
 2088        {
 2089            if (_disposed)
 2090            {
 2091                return;
 2092            }
 2093
 2094            foreach (var session in _activeConnections.Values)
 2095            {
 2096                await session.DisposeAsync().ConfigureAwait(false);
 2097            }
 2098
 2099            if (_idleTimer is not null)
 2100            {
 2101                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 2102                _idleTimer = null;
 2103            }
 2104
 2105            if (_inactiveTimer is not null)
 2106            {
 2107                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 2108                _inactiveTimer = null;
 2109            }
 2110
 2111            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2112
 2113            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 2114            _disposed = true;
 2115        }
 2116
 2117        private async void OnApplicationStopping()
 2118        {
 2119            _logger.LogInformation("Sending shutdown notifications");
 2120            try
 2121            {
 2122                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2123
 2124                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 2125            }
 2126            catch (Exception ex)
 2127            {
 2128                _logger.LogError(ex, "Error sending server shutdown notifications");
 2129            }
 2130
 2131            // Close open websockets to allow Kestrel to shut down cleanly
 2132            foreach (var session in _activeConnections.Values)
 2133            {
 2134                await session.DisposeAsync().ConfigureAwait(false);
 2135            }
 2136
 2137            _activeConnections.Clear();
 2138            _activeLiveStreamSessions.Clear();
 2139        }
 2140    }
 2141}

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)